Skip to main content

sokoban_elements/
tile.rs

1/// Shape of single game board tile.
2///
3/// In different [`Tessellation`](crate::Tessellation)s, board tiles have different shapes.
4///
5/// Shape of board tile can also depend on position of that tile on game board.
6///
7/// For example, in [`Trioban`](crate::Tessellation::Trioban), origin of coordinate system is
8/// triangle pointing upwards. Shape of all other triangles depends on orientation of origin and may
9/// be either triangle pointing upwards or triangle pointing downwards.
10///
11/// In [`Octoban`](crate::Tessellation::Octoban) tessellation, board is laid on alternating squares
12/// and octagons. Again, shape of each tile depends on position of that tile.
13#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum TileShape {
16    Square,
17    Hexagon,
18    TriangleUp,
19    TriangleDown,
20    Octagon,
21}
22
23/// What kind of game board element is one tile of that board.
24#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum TileKind {
27    Box,
28    BoxOnGoal,
29    Pusher,
30    PusherOnGoal,
31    #[default]
32    Floor,
33    Goal,
34    Wall,
35}
36
37impl TileKind {
38    /// Tile is a wall.
39    pub fn is_wall(&self) -> bool { *self == Self::Wall }
40
41    /// Tile carries a pusher, whether or not it also carries a goal.
42    pub fn has_pusher(&self) -> bool { *self == Self::Pusher || *self == Self::PusherOnGoal }
43
44    /// Tile carries a box, whether or not it also carries a goal.
45    pub fn has_box(&self) -> bool { *self == Self::Box || *self == Self::BoxOnGoal }
46
47    /// Tile carries a goal, whether or not a box or a pusher stands on it.
48    ///
49    /// [`BoxOnGoal`](Self::BoxOnGoal) and [`PusherOnGoal`](Self::PusherOnGoal) both match. Because
50    /// of that, counting goals and counting boxes are two independent scans of a board and the
51    /// n-th box is not the counterpart of the n-th goal.
52    pub fn has_goal(&self) -> bool {
53        *self == Self::Goal || *self == Self::BoxOnGoal || *self == Self::PusherOnGoal
54    }
55
56    /// Tile holds nothing at all: no wall, no piece and no goal.
57    ///
58    /// This is [`Floor`](Self::Floor), the [`Default`](std::default::Default) variant, and it is
59    /// the only tile kind that carries no board content. Its negation is exactly what
60    /// [`GameBoard::trim()`](crate::GameBoard::trim()) retains, and a board of nothing but empty
61    /// floor is what [`GameBoard::is_blank()`](crate::GameBoard::is_blank()) reports.
62    pub fn is_empty_floor(&self) -> bool { *self == Self::Floor }
63
64    /// Tile may be the first non-empty tile of a board row.
65    ///
66    /// This is a `SokobanYASC` board condition: the first non-empty tile in each row must be a wall
67    /// or a box standing on a goal. A plain [`Box`](Self::Box) does **not** qualify, and neither
68    /// does a lone goal or a pusher.
69    ///
70    /// Nothing in this crate enforces the condition - [`GameBoard`](crate::GameBoard) places no
71    /// restrictions on itself, and boards are edited long before they are played. The predicate is
72    /// here for callers that do want to check it.
73    pub fn can_start_a_row(&self) -> bool { self.is_wall() || *self == Self::BoxOnGoal }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    const ALL_KINDS: [TileKind; 7] = [
81        TileKind::Box,
82        TileKind::BoxOnGoal,
83        TileKind::Pusher,
84        TileKind::PusherOnGoal,
85        TileKind::Floor,
86        TileKind::Goal,
87        TileKind::Wall,
88    ];
89
90    // Every kind the predicate accepts, so each test pins a whole membership set rather than a
91    /// few hand-picked variants.
92    fn matching(predicate: impl Fn(&TileKind) -> bool) -> Vec<TileKind> {
93        ALL_KINDS
94            .into_iter()
95            .filter(|kind| predicate(kind))
96            .collect()
97    }
98
99    #[test]
100    fn only_the_wall_is_a_wall() {
101        assert_eq!(matching(TileKind::is_wall), vec![TileKind::Wall]);
102    }
103
104    #[test]
105    fn a_pusher_on_a_goal_still_has_a_pusher() {
106        assert_eq!(
107            matching(TileKind::has_pusher),
108            vec![TileKind::Pusher, TileKind::PusherOnGoal]
109        );
110    }
111
112    #[test]
113    fn a_box_on_a_goal_still_has_a_box() {
114        assert_eq!(matching(TileKind::has_box), vec![TileKind::Box, TileKind::BoxOnGoal]);
115    }
116
117    #[test]
118    fn an_occupied_goal_still_has_a_goal() {
119        assert_eq!(
120            matching(TileKind::has_goal),
121            vec![TileKind::BoxOnGoal, TileKind::PusherOnGoal, TileKind::Goal]
122        );
123    }
124
125    #[test]
126    fn only_the_floor_is_empty_floor() {
127        assert_eq!(matching(TileKind::is_empty_floor), vec![TileKind::Floor]);
128    }
129
130    #[test]
131    fn the_default_tile_is_empty_floor() {
132        assert!(TileKind::default().is_empty_floor());
133    }
134
135    #[test]
136    fn only_walls_and_boxes_on_goals_can_start_a_row() {
137        assert_eq!(
138            matching(TileKind::can_start_a_row),
139            vec![TileKind::BoxOnGoal, TileKind::Wall]
140        );
141    }
142
143    #[test]
144    fn a_plain_box_cannot_start_a_row() {
145        assert!(!TileKind::Box.can_start_a_row());
146        assert!(TileKind::BoxOnGoal.can_start_a_row());
147    }
148}