Skip to main content

sokoban_elements/
tessellation.rs

1mod errors;
2mod hexoban;
3mod octoban;
4mod sokoban;
5mod trioban;
6
7use std::fmt::Debug;
8use std::hash::Hash;
9
10use errors::{TessellationError, validate_and_apply_delta};
11
12use crate::game_board::BoardTransform;
13use crate::{Direction, DirectionsSet, Size2D, TileShape};
14
15/// Sokoban game variants.
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum Tessellation {
19    /// Classic Sokoban game variant.
20    ///
21    /// Game board is grid of squares.
22    ///
23    /// [`Direction`](crate::Direction) <-> character mapping:
24    ///
25    /// | Direction          | Characters |
26    /// | ------------------ | ---------- |
27    /// | `Direction::Left`  | `l`, `L`   |
28    /// | `Direction::Right` | `r`, `R`   |
29    /// | `Direction::Up`    | `u`, `U`   |
30    /// | `Direction::Down`  | `d`, `D`   |
31    ///
32    /// Example board
33    ///
34    /// ![Classic sokoban board](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/classic_sokoban_board.png)
35    #[default]
36    Sokoban,
37
38    /// Hexoban game variant.
39    ///
40    /// Game board is grid of vertical, equilateral hexagons with following coordinate system:
41    ///
42    /// ![Hexoban coordinates](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/hexoban_coordinates.png)
43    ///
44    /// Textual representation uses two characters for each hexagon. This allows different encoding
45    /// schemes.
46    ///
47    /// | Scheme 1             | Scheme 2             |
48    /// | -------------------- | -------------------- |
49    /// | ![][hexoban_scheme1] | ![][hexoban_scheme2] |
50    ///
51    /// [hexoban_scheme1]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/hexoban_scheme1.png
52    /// [hexoban_scheme2]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/hexoban_scheme2.png
53    ///
54    /// As long as encoding of single board is consistent, all methods handle any scheme
55    /// transparently - parsing of board strings 'Just Works ™'.
56    ///
57    /// [`Direction`](crate::Direction) <-> character mapping:
58    ///
59    /// | Direction              | Characters |
60    /// | ---------------------- | ---------- |
61    /// | `Direction::Left`      | `l`, `L`   |
62    /// | `Direction::Right`     | `r`, `R`   |
63    /// | `Direction::NorthWest` | `u`, `U`   |
64    /// | `Direction::SouthWest` | `d`, `D`   |
65    /// | `Direction::NorthEast` | `n`, `N`   |
66    /// | `Direction::SouthEast` | `s`, `S`   |
67    ///
68    /// Example board
69    ///
70    /// ![Hexoban puzzle](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/hexoban_example_board.png)
71    Hexoban,
72
73    /// Trioban game variant.
74    ///
75    /// Game board is grid of alternating triangles with origin triangle pointing down.
76    ///
77    /// Depending on pusher position, not all move directions are allowed on all board positions:
78    ///
79    /// ![Trioban movement](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/trioban_am.png)
80    ///
81    /// [`Direction`](crate::Direction) <-> character mapping:
82    ///
83    /// | Direction              | Characters |
84    /// | ---------------------- | ---------- |
85    /// | `Direction::Left`      | `l`, `L`   |
86    /// | `Direction::Right`     | `r`, `R`   |
87    /// | `Direction::NorthWest` | `u`, `U`   |
88    /// | `Direction::SouthWest` | `s`, `S`   |
89    /// | `Direction::NorthEast` | `n`, `N`   |
90    /// | `Direction::SouthEast` | `d`, `D`   |
91    ///
92    /// Example board
93    ///
94    /// ![Trioban puzzle](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/trioban_example_board.png)
95    Trioban,
96
97    /// Octoban game variant.
98    ///
99    /// Game board is grid of alternating squares and octagons with origin of coordinate system
100    /// being octagon.
101    ///
102    /// ![Octoban coordinates](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/octoban_coordinates.png)
103    ///
104    /// This game variant allows usage of all 8 directions of movement from
105    /// [`Direction`](crate::Direction). Depending on current pusher position, some of these
106    /// directions may not result in successful move or may even be invalid (ie. trying to move
107    /// north-west while standing on square tile).
108    ///
109    /// [`Direction`](crate::Direction) <-> character mapping:
110    ///
111    /// | Direction              | Characters |
112    /// | ---------------------- | ---------- |
113    /// | `Direction::Up`        | `u`, `U`   |
114    /// | `Direction::NorthEast` | `n`, `N`   |
115    /// | `Direction::Right`     | `r`, `R`   |
116    /// | `Direction::SouthEast` | `e`, `E`   |
117    /// | `Direction::Down`      | `d`, `D`   |
118    /// | `Direction::SouthWest` | `s`, `S`   |
119    /// | `Direction::Left`      | `l`, `L`   |
120    /// | `Direction::NorthWest` | `w`, `W`   |
121    ///
122    /// Example board
123    ///
124    /// ![Octoban puzzle](https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/octoban_example_board.png)
125    Octoban,
126}
127
128impl Tessellation {
129    /// Number of all possible [`Tessellation`]s.
130    pub const COUNT: usize = 4;
131
132    /// All possible [`Tessellation`]s in order of their discriminant values.
133    pub const ALL: [Tessellation; Tessellation::COUNT] = [
134        Tessellation::Sokoban,
135        Tessellation::Hexoban,
136        Tessellation::Trioban,
137        Tessellation::Octoban,
138    ];
139
140    /// Angle of one call to [`GameBoard::rotate()`](crate::GameBoard::rotate), in degrees.
141    pub fn rotation_step_degrees(&self) -> u16 {
142        match self {
143            Tessellation::Sokoban => sokoban::ROTATION_STEP_DEGREES,
144            Tessellation::Hexoban => hexoban::ROTATION_STEP_DEGREES,
145            Tessellation::Trioban => trioban::ROTATION_STEP_DEGREES,
146            Tessellation::Octoban => octoban::ROTATION_STEP_DEGREES,
147        }
148    }
149
150    /// Smallest axis-aligned integer translation that preserves the tiling.
151    ///
152    /// Board resizing may apply a smaller shift by adding perpendicular padding or, for Hexoban,
153    /// reindexing alternating rows.
154    pub fn translation_period(&self) -> Size2D {
155        match self {
156            Tessellation::Sokoban => Size2D::new(1, 1),
157            Tessellation::Hexoban => Size2D::new(1, 2),
158            Tessellation::Trioban | Tessellation::Octoban => Size2D::new(2, 2),
159        }
160    }
161
162    /// `true` if translation by `(dx, dy)` preserves the tiling.
163    pub(crate) fn is_rigid_translation(self, dx: i64, dy: i64) -> bool {
164        match self {
165            Tessellation::Sokoban => sokoban::is_rigid_translation(dx, dy),
166            Tessellation::Hexoban => hexoban::is_rigid_translation(dx, dy),
167            Tessellation::Trioban => trioban::is_rigid_translation(dx, dy),
168            Tessellation::Octoban => octoban::is_rigid_translation(dx, dy),
169        }
170    }
171
172    /// Result of applying `transform` to `(x, y)`.
173    pub(crate) fn transformed_raw(self, transform: BoardTransform, x: i64, y: i64) -> (i64, i64) {
174        match self {
175            Tessellation::Sokoban => sokoban::transformed_raw(transform, x, y),
176            Tessellation::Hexoban => hexoban::transformed_raw(transform, x, y),
177            Tessellation::Trioban => trioban::transformed_raw(transform, x, y),
178            Tessellation::Octoban => octoban::transformed_raw(transform, x, y),
179        }
180    }
181
182    /// Slice of all directions that are supported by this tessellation.
183    pub fn known_directions(&self) -> &'static [Direction] {
184        match self {
185            Tessellation::Sokoban => &sokoban::KNOWN_DIRECTIONS,
186            Tessellation::Trioban => &trioban::KNOWN_DIRECTIONS,
187            Tessellation::Octoban => &octoban::KNOWN_DIRECTIONS,
188            Tessellation::Hexoban => &hexoban::KNOWN_DIRECTIONS,
189        }
190    }
191
192    /// Slice of all tile shapes that are supported by this tessellation.
193    pub fn tile_shapes(&self) -> &'static [TileShape] {
194        match self {
195            Tessellation::Sokoban => &sokoban::TILE_SHAPES,
196            Tessellation::Trioban => &trioban::TILE_SHAPES,
197            Tessellation::Octoban => &octoban::TILE_SHAPES,
198            Tessellation::Hexoban => &hexoban::TILE_SHAPES,
199        }
200    }
201
202    /// [`TileShape`] for given game board position.
203    ///
204    /// Doesn't check if `(x, y)` is off board position because planar tiling applies to whole 2D
205    /// plane, not just to some part of it enclosed by some game board.
206    pub const fn tile_shape_from_pos(&self, x: usize, y: usize) -> TileShape {
207        match self {
208            Tessellation::Sokoban => sokoban::tile_shape_from_pos(),
209            Tessellation::Hexoban => hexoban::tile_shape_from_pos(),
210            Tessellation::Octoban => octoban::tile_shape_from_pos(x, y),
211            Tessellation::Trioban => trioban::tile_shape_from_pos(x, y),
212        }
213    }
214
215    /// Calculates neighbor position in part of plane enclosed by some game board.
216    ///
217    /// It returns `None` if:
218    ///
219    /// - input coordinate `(x, y)` is off-board coordinate
220    /// - output coordinate in direction `to` would've been off-board coordinate
221    /// - `to` is not supported direction of movement for underlying [`Tessellation`] (for example,
222    ///   trying to move north-west in [`Tessellation::Sokoban`] from any position)
223    /// - `to` is supported direction of movement for underlying [`Tessellation`] but is not allowed
224    ///   for `from` (for example, trying to move north-west in [`Tessellation::Octoban`] from
225    ///   position of square tile)
226    pub fn neighbor(
227        &self, x: usize, y: usize, to: Direction, width: usize, height: usize,
228    ) -> Option<(usize, usize)> {
229        match self {
230            Tessellation::Sokoban => sokoban::neighbor_checked(x, y, to, width, height).ok(),
231            Tessellation::Hexoban => hexoban::neighbor_checked(x, y, to, width, height).ok(),
232            Tessellation::Octoban => octoban::neighbor_checked(x, y, to, width, height).ok(),
233            Tessellation::Trioban => trioban::neighbor_checked(x, y, to, width, height).ok(),
234        }
235    }
236
237    /// Calculates set of directions where all directions in output set lead to valid
238    /// [`Self::neighbor()`] positions.
239    ///
240    /// If input coordinate `(x, y)` is off board position, returns `None`.
241    pub fn neighbor_directions(
242        &self, x: usize, y: usize, width: usize, height: usize,
243    ) -> Option<DirectionsSet> {
244        (x < width && y < height).then(|| {
245            DirectionsSet::from_iter(
246                self.known_directions()
247                    .iter()
248                    .filter_map(|d| self.neighbor(x, y, *d, width, height).map(|_| *d)),
249            )
250        })
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    /// Board size used by all dispatch tests.
259    ///
260    /// Chosen so that positions `(2, 2)` and `(3, 2)` have every one of their neighbors on-board in
261    /// every tessellation, which keeps `None` in the expectations below meaning "this direction is
262    /// not legal here" rather than "we walked off the edge".
263    const WIDTH: usize = 5;
264    const HEIGHT: usize = 5;
265
266    /// `(x, y, tile shape, neighbor)` in each of [`Direction::ALL`]
267    type PositionExpectation =
268        (usize, usize, TileShape, [Option<(usize, usize)>; Direction::COUNT]);
269
270    /// Hand-computed expectations, one row per position, derived from the coordinate systems
271    /// documented on the [`Tessellation`] variants rather than from the implementation modules.
272    ///
273    /// Every row covers all eight directions, so directions a tessellation does not support are
274    /// pinned as `None` and cannot silently start working.
275    #[rustfmt::skip]
276    const EXPECTATIONS: [(Tessellation, [PositionExpectation; 2]); 4] = [
277        //                                   Up            NorthEast     Right         SouthEast     Down          SouthWest     Left          NorthWest
278        (Tessellation::Sokoban, [
279            (2, 2, TileShape::Square,       [Some((2, 1)), None,         Some((3, 2)), None,         Some((2, 3)), None,         Some((1, 2)), None]),
280            (3, 2, TileShape::Square,       [Some((3, 1)), None,         Some((4, 2)), None,         Some((3, 3)), None,         Some((2, 2)), None]),
281        ]),
282        (Tessellation::Hexoban, [
283            (2, 2, TileShape::Hexagon,      [None,         Some((2, 1)), Some((3, 2)), Some((2, 3)), None,         Some((1, 3)), Some((1, 2)), Some((1, 1))]),
284            (3, 2, TileShape::Hexagon,      [None,         Some((3, 1)), Some((4, 2)), Some((3, 3)), None,         Some((2, 3)), Some((2, 2)), Some((2, 1))]),
285        ]),
286        (Tessellation::Trioban, [
287            (2, 2, TileShape::TriangleDown, [None,         Some((2, 1)), Some((3, 2)), Some((3, 2)), None,         Some((1, 2)), Some((1, 2)), Some((2, 1))]),
288            (3, 2, TileShape::TriangleUp,   [None,         Some((4, 2)), Some((4, 2)), Some((3, 3)), None,         Some((3, 3)), Some((2, 2)), Some((2, 2))]),
289        ]),
290        (Tessellation::Octoban, [
291            (2, 2, TileShape::Octagon,      [Some((2, 1)), Some((3, 1)), Some((3, 2)), Some((3, 3)), Some((2, 3)), Some((1, 3)), Some((1, 2)), Some((1, 1))]),
292            (3, 2, TileShape::Square,       [Some((3, 1)), None,         Some((4, 2)), None,         Some((3, 3)), None,         Some((2, 2)), None]),
293        ]),
294    ];
295
296    /// The four `match self` blocks in [`Tessellation`] are the only place a variant is bound to
297    /// its implementation module, and every arm has the same shape — mixing two of them up
298    /// compiles cleanly and is invisible on a read-through. This pins each variant to
299    /// hand-computed behavior that no two tessellations share.
300    #[test]
301    fn it_dispatches_to_the_behavior_of_its_own_tessellation() {
302        for (tessellation, positions) in EXPECTATIONS {
303            for (x, y, tile_shape, neighbors) in positions {
304                assert_eq!(
305                    tessellation.tile_shape_from_pos(x, y),
306                    tile_shape,
307                    "{tessellation:?} tile shape at ({x}, {y})"
308                );
309
310                for (direction, expected) in Direction::ALL.into_iter().zip(neighbors) {
311                    assert_eq!(
312                        tessellation.neighbor(x, y, direction, WIDTH, HEIGHT),
313                        expected,
314                        "{tessellation:?} neighbor of ({x}, {y}) to {direction}"
315                    );
316                }
317            }
318        }
319    }
320
321    #[test]
322    fn it_dispatches_transform_geometry_to_its_own_tessellation() {
323        #[rustfmt::skip]
324        let expectations = [
325            (Tessellation::Sokoban, 90, Size2D::new(1, 1), true,  [(2, -1), (-2, 1), (-1, 2), (1, -2)]),
326            (Tessellation::Hexoban, 60, Size2D::new(1, 2), false, [(2, -1), (-3, 1), (0, 3),  (2, -2)]),
327            (Tessellation::Trioban, 60, Size2D::new(2, 2), true,  [(2, -2), (-4, 1), (-2, 2), (3, -1)]),
328            (Tessellation::Octoban, 90, Size2D::new(2, 2), true,  [(2, -1), (-2, 1), (-1, 2), (1, -2)]),
329        ];
330        let transforms = [
331            BoardTransform::MirrorHorizontal,
332            BoardTransform::MirrorVertical,
333            BoardTransform::RotateClockwise,
334            BoardTransform::RotateCounterClockwise,
335        ];
336
337        for (tessellation, degrees, period, diagonal_is_rigid, transformed) in expectations {
338            assert_eq!(tessellation.rotation_step_degrees(), degrees);
339            assert_eq!(tessellation.translation_period(), period);
340            assert_eq!(tessellation.is_rigid_translation(1, 1), diagonal_is_rigid);
341            for (transform, expected) in transforms.into_iter().zip(transformed) {
342                assert_eq!(tessellation.transformed_raw(transform, 2, 1), expected);
343            }
344        }
345    }
346
347    /// [`Tessellation::neighbor_directions()`] is derived state: a direction belongs in the set
348    /// exactly when [`Tessellation::neighbor()`] yields a position for it, so the same
349    /// hand-computed rows pin it too.
350    ///
351    /// This says little about [`Tessellation::known_directions()`] on its own. A *shorter* list
352    /// than the tessellation needs does show up here, as directions missing from the set. A
353    /// longer one does not — the surplus directions are filtered out by
354    /// [`Tessellation::neighbor()`] anyway.
355    /// [`Self::it_agrees_with_its_implementation_module_over_the_whole_board`] is what checks the
356    /// list itself.
357    #[test]
358    fn its_neighbor_directions_are_the_directions_that_have_a_neighbor() {
359        for (tessellation, positions) in EXPECTATIONS {
360            for (x, y, _, neighbors) in positions {
361                let expected = DirectionsSet::from_iter(
362                    Direction::ALL
363                        .into_iter()
364                        .zip(neighbors)
365                        .filter_map(|(direction, neighbor)| neighbor.map(|_| direction)),
366                );
367
368                assert_eq!(
369                    tessellation.neighbor_directions(x, y, WIDTH, HEIGHT),
370                    Some(expected),
371                    "{tessellation:?} neighbor directions at ({x}, {y})"
372                );
373            }
374        }
375    }
376
377    type TileShapeFn = fn(usize, usize) -> TileShape;
378    type NeighborFn =
379        fn(usize, usize, Direction, usize, usize) -> Result<(usize, usize), TessellationError>;
380
381    /// The implementation module a [`Tessellation`] variant is supposed to dispatch to.
382    struct Module {
383        tessellation:        Tessellation,
384        known_directions:    &'static [Direction],
385        tile_shapes:         &'static [TileShape],
386        tile_shape_from_pos: TileShapeFn,
387        neighbor_checked:    NeighborFn,
388    }
389
390    /// Broader than the hand-computed rows above: sweeps the whole board and asserts that each
391    /// variant agrees with its module everywhere, not just at the two anchor positions. Also the
392    /// only coverage of [`Tessellation::known_directions()`] and [`Tessellation::tile_shapes()`].
393    ///
394    /// On its own this table could be miswired the same way the dispatch is; it is
395    /// [`Self::it_dispatches_to_the_behavior_of_its_own_tessellation`] that anchors which module
396    /// belongs to which variant.
397    #[test]
398    fn it_agrees_with_its_implementation_module_over_the_whole_board() {
399        let modules: [Module; 4] = [
400            Module {
401                tessellation:        Tessellation::Sokoban,
402                known_directions:    &sokoban::KNOWN_DIRECTIONS,
403                tile_shapes:         &sokoban::TILE_SHAPES,
404                tile_shape_from_pos: |_x, _y| sokoban::tile_shape_from_pos(),
405                neighbor_checked:    sokoban::neighbor_checked,
406            },
407            Module {
408                tessellation:        Tessellation::Hexoban,
409                known_directions:    &hexoban::KNOWN_DIRECTIONS,
410                tile_shapes:         &hexoban::TILE_SHAPES,
411                tile_shape_from_pos: |_x, _y| hexoban::tile_shape_from_pos(),
412                neighbor_checked:    hexoban::neighbor_checked,
413            },
414            Module {
415                tessellation:        Tessellation::Trioban,
416                known_directions:    &trioban::KNOWN_DIRECTIONS,
417                tile_shapes:         &trioban::TILE_SHAPES,
418                tile_shape_from_pos: trioban::tile_shape_from_pos,
419                neighbor_checked:    trioban::neighbor_checked,
420            },
421            Module {
422                tessellation:        Tessellation::Octoban,
423                known_directions:    &octoban::KNOWN_DIRECTIONS,
424                tile_shapes:         &octoban::TILE_SHAPES,
425                tile_shape_from_pos: octoban::tile_shape_from_pos,
426                neighbor_checked:    octoban::neighbor_checked,
427            },
428        ];
429
430        for module in modules {
431            let tessellation = module.tessellation;
432
433            assert_eq!(
434                tessellation.known_directions(),
435                module.known_directions,
436                "{tessellation:?}"
437            );
438            assert_eq!(tessellation.tile_shapes(), module.tile_shapes, "{tessellation:?}");
439
440            for y in 0..HEIGHT {
441                for x in 0..WIDTH {
442                    assert_eq!(
443                        tessellation.tile_shape_from_pos(x, y),
444                        (module.tile_shape_from_pos)(x, y),
445                        "{tessellation:?} tile shape at ({x}, {y})"
446                    );
447
448                    for direction in Direction::ALL {
449                        assert_eq!(
450                            tessellation.neighbor(x, y, direction, WIDTH, HEIGHT),
451                            (module.neighbor_checked)(x, y, direction, WIDTH, HEIGHT).ok(),
452                            "{tessellation:?} neighbor of ({x}, {y}) to {direction}"
453                        );
454                    }
455                }
456            }
457        }
458    }
459
460    /// Load-bearing for any game engine using our [`Tessellation`] and [`Direction`]
461    /// implementations.
462    ///
463    /// For example, inverting a reverse mode game's steps flips every direction with
464    /// [`Direction::opposite()`]. That is sound only if stepping back that way lands exactly where
465    /// the step came from. Breaking this test silently breaks any engine that depends on that
466    /// guarantee.
467    ///
468    /// Nothing in the geometry modules enforces it. Each tessellation writes its own delta per
469    /// direction, Hexoban's depend on row parity and Trioban's on tile shape, and Trioban's six
470    /// direction names alias onto three real neighbors per triangle - so the property is pinned
471    /// here rather than assumed.
472    #[test]
473    fn opposite_is_the_geometric_inverse_of_neighbor() {
474        for (tessellation, _) in EXPECTATIONS {
475            for y in 0..HEIGHT {
476                for x in 0..WIDTH {
477                    // Illegal directions yield `None` and are skipped, so this covers every
478                    // position/direction pair the tessellation actually admits.
479                    for direction in Direction::ALL {
480                        let Some((neighbor_x, neighbor_y)) =
481                            tessellation.neighbor(x, y, direction, WIDTH, HEIGHT)
482                        else {
483                            continue;
484                        };
485
486                        assert_eq!(
487                            tessellation.neighbor(
488                                neighbor_x,
489                                neighbor_y,
490                                direction.opposite(),
491                                WIDTH,
492                                HEIGHT
493                            ),
494                            Some((x, y)),
495                            "{tessellation:?}: ({x}, {y}) to {direction} lands on \
496                            ({neighbor_x}, {neighbor_y}), which does not step back to it"
497                        );
498                    }
499                }
500            }
501        }
502    }
503
504    /// The other half of what inverting a step sequence needs: a flipped direction has to be one
505    /// the tessellation still accepts, or
506    /// [`SavedGame::from_steps()`](crate::SavedGame::from_steps()) would reject the converted
507    /// game.
508    #[test]
509    fn known_directions_are_closed_under_opposite() {
510        for (tessellation, _) in EXPECTATIONS {
511            for direction in tessellation.known_directions() {
512                assert!(
513                    tessellation
514                        .known_directions()
515                        .contains(&direction.opposite()),
516                    "{:?} knows {} but not its opposite {}",
517                    tessellation,
518                    direction,
519                    direction.opposite()
520                );
521            }
522        }
523    }
524
525    #[test]
526    fn it_has_no_neighbor_directions_off_board() {
527        for (tessellation, _) in EXPECTATIONS {
528            assert!(
529                tessellation
530                    .neighbor_directions(WIDTH, 0, WIDTH, HEIGHT)
531                    .is_none()
532            );
533            assert!(
534                tessellation
535                    .neighbor_directions(0, HEIGHT, WIDTH, HEIGHT)
536                    .is_none()
537            );
538
539            for direction in Direction::ALL {
540                assert!(
541                    tessellation
542                        .neighbor(WIDTH, 0, direction, WIDTH, HEIGHT)
543                        .is_none()
544                );
545                assert!(
546                    tessellation
547                        .neighbor(0, HEIGHT, direction, WIDTH, HEIGHT)
548                        .is_none()
549                );
550            }
551        }
552    }
553}