Skip to main content

sokoban_elements/piece/
piece_id.rs

1const MAX_BOXES_COUNT: u16 = 4096;
2const MAX_PUSHERS_COUNT: u16 = 512;
3
4use std::fmt::{Display, Formatter};
5
6use super::errors::{PieceIdError, PieceIdErrorKind};
7
8/// Box identifier.
9///
10/// It can't be larger than `BoxID::MAX`.
11///
12/// # Assigning and managing identifiers for game board pieces
13///
14/// Each game piece is referred to by it's unique `ID`. These IDs are simple integers assigned in
15/// consecutive order by scanning board top to bottom, and scanning each row from left to right.
16///
17/// | Assigning piece IDs |
18/// | ------------------- |
19/// | ![][assigning_ids]  |
20///
21/// [assigning_ids]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/assigning_ids.png
22///
23/// When editing game boards, these IDs are necessarily transient: they change in the process as
24/// pieces are being added or removed. During game solving session, these IDs are fixed - referring
25/// to box with ID `1` will always refer to the exact same box. Position of that box may change, but
26/// it's identifier remains constant and attached to it. Same reasoning applies to goals and
27/// pushers.
28///
29/// Mirroring, rotating, resizing, or trimming a board can therefore invalidate IDs and every
30/// [`SokobanPlus`](crate::SokobanPlus) or [`SavedGame`](crate::SavedGame) keyed to that board.
31///
32/// | Initial board                | Board was edited             | Box was moved during game play |
33/// | ---------------------------- | ---------------------------- | ------------------------------ |
34/// | ![][movement_vs_transfer_01] | ![][movement_vs_transfer_02] | ![][movement_vs_transfer_03]   |
35///
36/// [movement_vs_transfer_01]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/movement_vs_transfer_01.png
37/// [movement_vs_transfer_02]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/movement_vs_transfer_02.png
38/// [movement_vs_transfer_03]: https://raw.githubusercontent.com/tadams42/sokoban-elements/main/crates/sokoban-elements/doc/img/movement_vs_transfer_03.png
39///
40/// # Serde
41///
42/// With the `serde` feature, this serializes as a bare integer and deserializes through
43/// [`TryFrom`](std::convert::TryFrom), so the `BoxID::MAX` bound is enforced on the way in. The
44/// same applies to [`GoalID`], [`PusherID`] and [`PlusID`](crate::PlusID).
45#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46#[cfg_attr(
47    feature = "serde",
48    derive(serde::Serialize, serde::Deserialize),
49    serde(try_from = "u16", into = "u16")
50)]
51pub struct BoxID(u16);
52
53/// Goal identifier.
54///
55/// See [`BoxID`] docs on how these identifiers are assigned and managed.
56///
57/// It can't be larger than `GoalID::MAX`.
58#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59#[cfg_attr(
60    feature = "serde",
61    derive(serde::Serialize, serde::Deserialize),
62    serde(try_from = "u16", into = "u16")
63)]
64pub struct GoalID(u16);
65
66/// Pusher identifier.
67///
68/// See [`BoxID`] docs on how these identifiers are assigned and managed.
69///
70/// It can't be larger than `PusherID::MAX`.
71#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72#[cfg_attr(
73    feature = "serde",
74    derive(serde::Serialize, serde::Deserialize),
75    serde(try_from = "u16", into = "u16")
76)]
77pub struct PusherID(u16);
78
79macro_rules! implement_piece_id_construct_from_int {
80    ( $PieceIdTy:ident, $ErrKind:expr, u8 ) => {
81        impl From<u8> for $PieceIdTy {
82            fn from(id: u8) -> Self { Self(id.into()) }
83        }
84    };
85
86    ( $PieceIdTy:ident, $ErrKind:expr, u16 ) => {
87        impl TryFrom<u16> for $PieceIdTy {
88            type Error = PieceIdError;
89            fn try_from(value: u16) -> Result<Self, Self::Error> {
90                if value > Self::MAX.into() {
91                    Err(PieceIdError {
92                        kind:  $ErrKind,
93                        value: value.to_string(),
94                    })
95                } else {
96                    Ok(Self(value))
97                }
98            }
99        }
100    };
101
102    ( $PieceIdTy:ident, $ErrKind:expr, $IntTy:ty ) => {
103        impl TryFrom<$IntTy> for $PieceIdTy {
104            type Error = PieceIdError;
105            fn try_from(value: $IntTy) -> Result<Self, Self::Error> {
106                u16::try_from(value)
107                    .map_err(|_| {
108                        PieceIdError {
109                            kind:  $ErrKind,
110                            value: value.to_string(),
111                        }
112                    })?
113                    .try_into()
114            }
115        }
116    };
117}
118
119macro_rules! implement_piece_id {
120    ( $PieceIdTy:ident, $MaxConst:ident, $ErrKind:expr ) => {
121        impl $PieceIdTy {
122            const FIRST: u16 = 0;
123            pub const MAX: u16 = $MaxConst - 1;
124
125            pub fn new(id: u16) -> Result<Self, PieceIdError> { id.try_into() }
126        }
127
128        impl Display for $PieceIdTy {
129            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }
130        }
131
132        impl Default for $PieceIdTy {
133            fn default() -> Self { Self($PieceIdTy::FIRST) }
134        }
135
136        // Convert into integer types
137
138        impl From<$PieceIdTy> for u16 {
139            fn from(value: $PieceIdTy) -> Self { return value.0; }
140        }
141
142        impl From<$PieceIdTy> for usize {
143            fn from(value: $PieceIdTy) -> Self { return value.0.into(); }
144        }
145
146        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, u8);
147        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, u16);
148        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, u32);
149        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, u64);
150        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, usize);
151
152        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, i8);
153        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, i16);
154        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, i32);
155        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, i64);
156        implement_piece_id_construct_from_int!($PieceIdTy, $ErrKind, isize);
157
158        // Compare to integer types, ie. `BoxID == u32`
159        //
160        // This should not be implemented: `u32 == BoxID`.
161        //
162        // https://doc.rust-lang.org/std/cmp/trait.PartialEq.html#cross-crate-considerations
163
164        impl PartialEq<u8> for $PieceIdTy {
165            fn eq(&self, other: &u8) -> bool { self.0 == u16::from(*other) }
166        }
167
168        impl PartialEq<u16> for $PieceIdTy {
169            fn eq(&self, other: &u16) -> bool { self.0 == *other }
170        }
171
172        impl PartialEq<u32> for $PieceIdTy {
173            fn eq(&self, other: &u32) -> bool { u32::from(self.0) == *other }
174        }
175
176        impl PartialEq<u64> for $PieceIdTy {
177            fn eq(&self, other: &u64) -> bool { u64::from(self.0) == *other }
178        }
179
180        impl PartialEq<usize> for $PieceIdTy {
181            fn eq(&self, other: &usize) -> bool { usize::from(self.0) == *other }
182        }
183
184        impl PartialEq<i8> for $PieceIdTy {
185            fn eq(&self, other: &i8) -> bool { i32::from(self.0) == i32::from(*other) }
186        }
187
188        impl PartialEq<i16> for $PieceIdTy {
189            fn eq(&self, other: &i16) -> bool { i32::from(self.0) == i32::from(*other) }
190        }
191
192        impl PartialEq<i32> for $PieceIdTy {
193            fn eq(&self, other: &i32) -> bool { i32::from(self.0) == *other }
194        }
195
196        impl PartialEq<i64> for $PieceIdTy {
197            fn eq(&self, other: &i64) -> bool { i64::from(self.0) == *other }
198        }
199
200        impl PartialEq<isize> for $PieceIdTy {
201            fn eq(&self, other: &isize) -> bool {
202                match isize::try_from(self.0) {
203                    Ok(self_isize) => self_isize == *other,
204                    Err(_) => false,
205                }
206            }
207        }
208    };
209}
210
211implement_piece_id!(BoxID, MAX_BOXES_COUNT, PieceIdErrorKind::InvalidBoxID);
212implement_piece_id!(GoalID, MAX_BOXES_COUNT, PieceIdErrorKind::InvalidGoalID);
213implement_piece_id!(PusherID, MAX_PUSHERS_COUNT, PieceIdErrorKind::InvalidPusherID);
214
215impl PartialEq<GoalID> for BoxID {
216    fn eq(&self, other: &GoalID) -> bool { self.0 == other.0 }
217}
218
219impl PartialEq<BoxID> for GoalID {
220    fn eq(&self, other: &BoxID) -> bool { self.0 == other.0 }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_box_id_can_be_constructed_from_integers() -> Result<(), PieceIdError> {
229        assert!(BoxID::new(42)? == 42_u8);
230
231        assert!(BoxID::from(42_u8) == 42_u8);
232        assert!(BoxID::try_from(42_u16)? == 42_u8);
233        assert!(BoxID::try_from(42_u32)? == 42_u8);
234        assert!(BoxID::try_from(42_u64)? == 42_u8);
235        assert!(BoxID::try_from(42_usize)? == 42_u8);
236
237        assert!(BoxID::try_from(42_i8)? == 42_u8);
238        assert!(BoxID::try_from(42_i16)? == 42_u8);
239        assert!(BoxID::try_from(42_i32)? == 42_u8);
240        assert!(BoxID::try_from(42_i64)? == 42_u8);
241        assert!(BoxID::try_from(42_isize)? == 42_u8);
242
243        assert!(BoxID::try_from(-1_i32).is_err());
244
245        Ok(())
246    }
247
248    #[test]
249    fn test_box_id_is_value_constrained() {
250        assert!(BoxID::try_from(BoxID::FIRST).is_ok());
251        assert!(BoxID::try_from(BoxID::MAX).is_ok());
252        assert!(BoxID::default() == BoxID::FIRST);
253        assert!(BoxID::try_from(usize::from(BoxID::MAX) + 1).is_err());
254        assert!(BoxID::try_from(MAX_BOXES_COUNT - 1).is_ok());
255        assert!(BoxID::try_from(MAX_BOXES_COUNT).is_err());
256    }
257
258    #[test]
259    fn test_box_id_is_comparable() {
260        assert!(BoxID::from(0) == BoxID::from(0));
261        assert!(BoxID::from(0) == 0_u8);
262        assert!(BoxID::from(0) == 0_u16);
263        assert!(BoxID::from(0) == 0_u32);
264        assert!(BoxID::from(0) == 0_u64);
265        assert!(BoxID::from(0) == 0_usize);
266        assert!(BoxID::from(0) == 0_i8);
267        assert!(BoxID::from(0) == 0_i16);
268        assert!(BoxID::from(0) == 0_i32);
269        assert!(BoxID::from(0) == 0_i64);
270        assert!(BoxID::from(0) == 0_isize);
271
272        assert!(BoxID::from(0) != BoxID::from(1));
273        assert!(BoxID::from(0) != 1_u8);
274        assert!(BoxID::from(0) != 1_u16);
275        assert!(BoxID::from(0) != 1_u32);
276        assert!(BoxID::from(0) != 1_u64);
277        assert!(BoxID::from(0) != 1_usize);
278        assert!(BoxID::from(0) != 1_i8);
279        assert!(BoxID::from(0) != 1_i16);
280        assert!(BoxID::from(0) != 1_i32);
281        assert!(BoxID::from(0) != 1_i64);
282        assert!(BoxID::from(0) != 1_isize);
283
284        assert!(BoxID::from(0) == GoalID::from(0));
285        assert!(BoxID::from(0) != GoalID::from(1));
286    }
287
288    #[test]
289    fn test_goal_id_can_be_constructed_from_integers() -> Result<(), PieceIdError> {
290        assert!(GoalID::new(42)? == 42_u8);
291
292        assert!(GoalID::from(42_u8) == 42_u8);
293        assert!(GoalID::try_from(42_u16)? == 42_u8);
294        assert!(GoalID::try_from(42_u32)? == 42_u8);
295        assert!(GoalID::try_from(42_u64)? == 42_u8);
296        assert!(GoalID::try_from(42_usize)? == 42_u8);
297
298        assert!(GoalID::try_from(42_i8)? == 42_u8);
299        assert!(GoalID::try_from(42_i16)? == 42_u8);
300        assert!(GoalID::try_from(42_i32)? == 42_u8);
301        assert!(GoalID::try_from(42_i64)? == 42_u8);
302        assert!(GoalID::try_from(42_isize)? == 42_u8);
303
304        assert!(GoalID::try_from(-1_i32).is_err());
305
306        Ok(())
307    }
308
309    #[test]
310    fn test_goal_id_is_value_constrained() {
311        assert!(GoalID::try_from(GoalID::FIRST).is_ok());
312        assert!(GoalID::try_from(GoalID::MAX).is_ok());
313        assert!(GoalID::default() == GoalID::FIRST);
314        assert!(GoalID::try_from(usize::from(GoalID::MAX) + 1).is_err());
315        assert!(GoalID::try_from(MAX_BOXES_COUNT - 1).is_ok());
316        assert!(GoalID::try_from(MAX_BOXES_COUNT).is_err());
317    }
318
319    #[test]
320    fn test_goal_id_is_comparable() {
321        assert!(GoalID::from(0) == GoalID::from(0));
322        assert!(GoalID::from(0) == 0_u8);
323        assert!(GoalID::from(0) == 0_u16);
324        assert!(GoalID::from(0) == 0_u32);
325        assert!(GoalID::from(0) == 0_u64);
326        assert!(GoalID::from(0) == 0_usize);
327        assert!(GoalID::from(0) == 0_i8);
328        assert!(GoalID::from(0) == 0_i16);
329        assert!(GoalID::from(0) == 0_i32);
330        assert!(GoalID::from(0) == 0_i64);
331        assert!(GoalID::from(0) == 0_isize);
332
333        assert!(GoalID::from(0) != GoalID::from(1));
334        assert!(GoalID::from(0) != 1_u8);
335        assert!(GoalID::from(0) != 1_u16);
336        assert!(GoalID::from(0) != 1_u32);
337        assert!(GoalID::from(0) != 1_u64);
338        assert!(GoalID::from(0) != 1_usize);
339        assert!(GoalID::from(0) != 1_i8);
340        assert!(GoalID::from(0) != 1_i16);
341        assert!(GoalID::from(0) != 1_i32);
342        assert!(GoalID::from(0) != 1_i64);
343        assert!(GoalID::from(0) != 1_isize);
344
345        assert!(GoalID::from(0) == BoxID::from(0));
346        assert!(GoalID::from(0) != BoxID::from(1));
347    }
348
349    #[test]
350    fn test_pusher_id_can_be_constructed_from_integers() -> Result<(), PieceIdError> {
351        assert!(PusherID::new(42)? == 42_u8);
352
353        assert!(PusherID::from(42_u8) == 42_u8);
354        assert!(PusherID::try_from(42_u16)? == 42_u8);
355        assert!(PusherID::try_from(42_u32)? == 42_u8);
356        assert!(PusherID::try_from(42_u64)? == 42_u8);
357        assert!(PusherID::try_from(42_usize)? == 42_u8);
358
359        assert!(PusherID::try_from(42_i8)? == 42_u8);
360        assert!(PusherID::try_from(42_i16)? == 42_u8);
361        assert!(PusherID::try_from(42_i32)? == 42_u8);
362        assert!(PusherID::try_from(42_i64)? == 42_u8);
363        assert!(PusherID::try_from(42_isize)? == 42_u8);
364
365        assert!(PusherID::try_from(-1_i32).is_err());
366
367        Ok(())
368    }
369
370    #[test]
371    fn test_pusher_id_is_value_constrained() {
372        assert!(PusherID::try_from(PusherID::FIRST).is_ok());
373        assert!(PusherID::try_from(PusherID::MAX).is_ok());
374        assert!(PusherID::default() == PusherID::FIRST);
375        assert!(PusherID::try_from(usize::from(PusherID::MAX) + 1).is_err());
376        assert!(PusherID::try_from(MAX_PUSHERS_COUNT - 1).is_ok());
377        assert!(PusherID::try_from(MAX_PUSHERS_COUNT).is_err());
378    }
379
380    #[test]
381    fn test_pusher_id_is_comparable() {
382        assert!(PusherID::from(0) == PusherID::from(0));
383        assert!(PusherID::from(0) == 0_u8);
384        assert!(PusherID::from(0) == 0_u16);
385        assert!(PusherID::from(0) == 0_u32);
386        assert!(PusherID::from(0) == 0_u64);
387        assert!(PusherID::from(0) == 0_usize);
388        assert!(PusherID::from(0) == 0_i8);
389        assert!(PusherID::from(0) == 0_i16);
390        assert!(PusherID::from(0) == 0_i32);
391        assert!(PusherID::from(0) == 0_i64);
392        assert!(PusherID::from(0) == 0_isize);
393
394        assert!(PusherID::from(0) != PusherID::from(1));
395        assert!(PusherID::from(0) != 1_u8);
396        assert!(PusherID::from(0) != 1_u16);
397        assert!(PusherID::from(0) != 1_u32);
398        assert!(PusherID::from(0) != 1_u64);
399        assert!(PusherID::from(0) != 1_usize);
400        assert!(PusherID::from(0) != 1_i8);
401        assert!(PusherID::from(0) != 1_i16);
402        assert!(PusherID::from(0) != 1_i32);
403        assert!(PusherID::from(0) != 1_i64);
404        assert!(PusherID::from(0) != 1_isize);
405    }
406
407    #[cfg(feature = "serde")]
408    mod test_piece_id_serde {
409        use std::error::Error;
410
411        use super::*;
412
413        #[test]
414        fn it_serializes_as_a_bare_integer() -> Result<(), Box<dyn Error>> {
415            assert_eq!(serde_json::to_string(&BoxID::from(42_u8))?, "42");
416            assert_eq!(serde_json::to_string(&GoalID::from(42_u8))?, "42");
417            assert_eq!(serde_json::to_string(&PusherID::from(42_u8))?, "42");
418
419            assert_eq!(serde_json::from_str::<BoxID>("42")?, BoxID::from(42_u8));
420            assert_eq!(serde_json::from_str::<GoalID>("42")?, GoalID::from(42_u8));
421            assert_eq!(serde_json::from_str::<PusherID>("42")?, PusherID::from(42_u8));
422
423            Ok(())
424        }
425
426        #[test]
427        fn it_rejects_ids_out_of_range() {
428            assert!(serde_json::from_str::<BoxID>(&MAX_BOXES_COUNT.to_string()).is_err());
429            assert!(serde_json::from_str::<GoalID>(&MAX_BOXES_COUNT.to_string()).is_err());
430            assert!(serde_json::from_str::<PusherID>(&MAX_PUSHERS_COUNT.to_string()).is_err());
431
432            assert!(serde_json::from_str::<BoxID>("65536").is_err());
433            assert!(serde_json::from_str::<BoxID>("-1").is_err());
434        }
435
436        #[test]
437        fn it_accepts_the_largest_legal_id() -> Result<(), Box<dyn Error>> {
438            assert_eq!(
439                serde_json::from_str::<BoxID>(&BoxID::MAX.to_string())?,
440                BoxID::try_from(BoxID::MAX)?
441            );
442            assert_eq!(
443                serde_json::from_str::<PusherID>(&PusherID::MAX.to_string())?,
444                PusherID::try_from(PusherID::MAX)?
445            );
446
447            Ok(())
448        }
449    }
450}