parking_game/
lib.rs

1// Copyright (c) Addison Crump, 2025, licensed under the EUPL-1.2-or-later.
2
3//! parking-game: a library recreating the rules of Thinkfun's "Rush Hour".
4//!
5//! This library implements the core movement rules of "Rush Hour", with a focus on memory and
6//! performance. The premise of the game is simple: you have one to many "cars" with fixed
7//! orientations (up/down or left/right) that can only move forwards and backwards in that
8//! orientation. You must move the designated car from its given start position to a desired end
9//! position by manipulating the other cars in the board. Cars may not intersect and they must stay
10//! within the bounds of the board. In this library, we only implement the _movement_ rules
11//! (intersection and bounds checks included); the gameplay is left to the user.
12
13#![no_std]
14
15use alloc::vec;
16use alloc::vec::Vec;
17use core::error::Error;
18use core::fmt::{Debug, Display, Formatter};
19use core::hash::{Hash, Hasher};
20use core::marker::PhantomData;
21use core::num::{IntErrorKind, NonZeroUsize};
22use core::ops::{Add, AddAssign, Deref, DerefMut, Neg, Sub, SubAssign};
23use num_traits::{CheckedAdd, CheckedMul, CheckedSub, One, Unsigned, Zero};
24use serde::de::DeserializeOwned;
25use serde::{Deserialize, Serialize};
26
27extern crate alloc;
28
29/// An orientation for a car.
30#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize)]
31pub enum Orientation {
32    /// The car may only move up and down.
33    UpDown,
34    /// The car may only move left and right.
35    LeftRight,
36}
37
38/// A direction for a move. A direction may be flipped with [`Neg`] (i.e. `-`).
39#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)]
40pub enum Direction {
41    /// Upward movement.
42    Up,
43    /// Downward movement.
44    Down,
45    /// Leftward movement.
46    Left,
47    /// Rightward movement.
48    Right,
49}
50
51impl Display for Direction {
52    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
53        f.write_str(match self {
54            Direction::Up => "up",
55            Direction::Down => "down",
56            Direction::Left => "left",
57            Direction::Right => "right",
58        })
59    }
60}
61
62impl Neg for Direction {
63    type Output = Self;
64
65    fn neg(self) -> Self::Output {
66        match self {
67            Direction::Up => Direction::Down,
68            Direction::Down => Direction::Up,
69            Direction::Left => Direction::Right,
70            Direction::Right => Direction::Left,
71        }
72    }
73}
74
75/// Marker trait: specifies that a value may be used for board definitions.
76pub trait BoardValue:
77    One
78    + Ord
79    + Add<Output = Self>
80    + CheckedAdd
81    + Sub<Output = Self>
82    + CheckedSub
83    + AddAssign
84    + SubAssign
85    + Copy
86    + Into<usize>
87    + TryFrom<usize>
88    + Zero
89    + CheckedMul
90    + Debug
91    + Display
92    + Unsigned
93    + DeserializeOwned
94    + Serialize
95    + 'static
96{
97}
98
99impl<V> BoardValue for V where
100    V: One
101        + Ord
102        + Add<Output = Self>
103        + CheckedAdd
104        + Sub<Output = Self>
105        + CheckedSub
106        + AddAssign
107        + SubAssign
108        + Copy
109        + Into<usize>
110        + TryFrom<usize>
111        + Zero
112        + CheckedMul
113        + Debug
114        + Display
115        + Unsigned
116        + DeserializeOwned
117        + Serialize
118        + 'static
119{
120}
121
122/// A car, generic over the numeric type which backs it. The numeric type must be unsigned and
123/// integral.
124#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize)]
125pub struct Car<V> {
126    length: V,
127    orientation: Orientation,
128}
129
130impl<V> Car<V> {
131    /// The length of the car.
132    pub fn length(&self) -> &V {
133        &self.length
134    }
135
136    /// The orientation of the car.
137    pub fn orientation(&self) -> Orientation {
138        self.orientation
139    }
140}
141
142impl<V> Car<V>
143where
144    V: BoardValue,
145{
146    /// Create a new car of the provided length and orientation.
147    pub fn new(length: V, orientation: Orientation) -> Option<Self> {
148        if length < V::one() {
149            None
150        } else {
151            Some(Self {
152                length,
153                orientation,
154            })
155        }
156    }
157}
158
159/// A position in the board (eff., a coordinate pair).
160#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize)]
161pub struct Position<V> {
162    row: V,
163    column: V,
164}
165
166impl<V> Position<V> {
167    /// The row of the position.
168    pub fn row(&self) -> &V {
169        &self.row
170    }
171
172    /// The column of the position.
173    pub fn column(&self) -> &V {
174        &self.column
175    }
176}
177
178impl<V> Add for Position<V>
179where
180    V: BoardValue,
181{
182    type Output = Self;
183
184    fn add(self, rhs: Self) -> Self::Output {
185        Self {
186            row: self.row + rhs.row,
187            column: self.column + rhs.column,
188        }
189    }
190}
191
192impl<V> CheckedAdd for Position<V>
193where
194    V: BoardValue,
195{
196    fn checked_add(&self, rhs: &Self) -> Option<Self> {
197        Some(Self {
198            row: self.row.checked_add(&rhs.row)?,
199            column: self.column.checked_add(&rhs.column)?,
200        })
201    }
202}
203
204impl<V> Sub for Position<V>
205where
206    V: BoardValue,
207{
208    type Output = Self;
209
210    fn sub(self, rhs: Self) -> Self::Output {
211        Self {
212            row: self.row - rhs.row,
213            column: self.column - rhs.column,
214        }
215    }
216}
217
218impl<V> CheckedSub for Position<V>
219where
220    V: BoardValue,
221{
222    fn checked_sub(&self, rhs: &Self) -> Option<Self> {
223        Some(Self {
224            row: self.row.checked_sub(&rhs.row)?,
225            column: self.column.checked_sub(&rhs.column)?,
226        })
227    }
228}
229
230impl<V> AddAssign for Position<V>
231where
232    V: BoardValue,
233{
234    fn add_assign(&mut self, rhs: Self) {
235        self.row += rhs.row;
236        self.column += rhs.column;
237    }
238}
239
240impl<V> Position<V>
241where
242    V: BoardValue,
243{
244    /// The position encoded as an index into an board with the provided dimensions.
245    pub fn as_index(&self, dim: &Dimensions<V>) -> Option<usize> {
246        if self.row >= dim.rows || self.column >= dim.columns {
247            return None;
248        }
249        let row = self.row.into();
250        let column = self.column.into();
251        Some(row * dim.columns.into() + column)
252    }
253}
254
255impl<V> Position<V>
256where
257    Self: CheckedAdd<Output = Self> + CheckedSub<Output = Self>,
258    V: BoardValue,
259{
260    /// Get the position `by` units away from this position in the provided direction `dir`, or
261    /// `None` if the position would be out of bounds.
262    pub fn shift(&self, dir: Direction, by: V) -> Option<Self> {
263        match dir {
264            Direction::Up => self.checked_sub(&Self::from((by, V::zero()))),
265            Direction::Down => self.checked_add(&Self::from((by, V::zero()))),
266            Direction::Left => self.checked_sub(&Self::from((V::zero(), by))),
267            Direction::Right => self.checked_add(&Self::from((V::zero(), by))),
268        }
269    }
270}
271
272impl<V> From<(V, V)> for Position<V> {
273    fn from((row, column): (V, V)) -> Self {
274        Self { row, column }
275    }
276}
277
278/// The dimensions of a parking game board in terms of rows and columns.
279#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize)]
280pub struct Dimensions<V> {
281    rows: V,
282    columns: V,
283}
284
285impl<V> Dimensions<V> {
286    /// The number of rows.
287    pub fn rows(&self) -> &V {
288        &self.rows
289    }
290
291    /// The number of columns.
292    pub fn columns(&self) -> &V {
293        &self.columns
294    }
295}
296
297/// An error associated with the creation of the dimensions.
298#[derive(Debug)]
299pub struct DimensionError(IntErrorKind);
300
301impl Display for DimensionError {
302    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
303        let reason = match self.0 {
304            IntErrorKind::PosOverflow => "the dimensions were too large",
305            IntErrorKind::Zero => "the dimensions have zero area",
306            _ => unreachable!(),
307        };
308        f.write_fmt(format_args!("dimensions could not be used: {reason}"))
309    }
310}
311
312impl Error for DimensionError {}
313
314impl<V> TryFrom<(V, V)> for Dimensions<V>
315where
316    V: BoardValue,
317{
318    type Error = DimensionError;
319
320    fn try_from((rows, columns): (V, V)) -> Result<Self, Self::Error> {
321        if let Some(size) = rows.checked_mul(&columns) {
322            if size.is_zero() {
323                Err(DimensionError(IntErrorKind::Zero))
324            } else {
325                Ok(Self { rows, columns })
326            }
327        } else {
328            Err(DimensionError(IntErrorKind::PosOverflow))
329        }
330    }
331}
332
333/// A state of the game. This is guaranteed to be a valid state as long as it is constructed with
334/// [`State::empty`] and manipulated with via [`Board`] operations.
335#[derive(Clone, Debug, Hash, Deserialize, Serialize)]
336pub struct State<V> {
337    dim: Dimensions<V>,
338    cars: Vec<(Position<V>, Car<V>)>,
339}
340
341impl<V> State<V> {
342    /// The dimensions of this state.
343    pub fn dimensions(&self) -> &Dimensions<V> {
344        &self.dim
345    }
346
347    /// The cars contained within this state.
348    pub fn cars(&self) -> &[(Position<V>, Car<V>)] {
349        &self.cars
350    }
351}
352
353impl<V> State<V> {
354    /// Produce an empty state (i.e., one with no cars) with the provided dimensions.
355    pub fn empty<D: TryInto<Dimensions<V>>>(dim: D) -> Result<Self, D::Error> {
356        let dim = dim.try_into()?;
357        Ok(Self {
358            dim,
359            cars: Vec::new(),
360        })
361    }
362}
363
364/// A type of invalid state, associated with an [`InvalidStateError`].
365#[derive(Debug)]
366pub enum InvalidStateType {
367    /// The car with the provided index is in an invalid position.
368    InvalidPosition(NonZeroUsize),
369    /// The cars with the provided indices overlap.
370    Overlap(NonZeroUsize, NonZeroUsize),
371}
372
373/// An error which denotes that an invalid state was encountered.
374#[derive(Debug)]
375pub struct InvalidStateError<V> {
376    position: Position<V>,
377    variant: InvalidStateType,
378}
379
380impl<V> Display for InvalidStateError<V>
381where
382    V: BoardValue,
383{
384    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
385        match self.variant {
386            InvalidStateType::InvalidPosition(idx) => f.write_fmt(format_args!(
387                "car {idx} was located at an invalid position ({}, {})",
388                self.position.row, self.position.column
389            )),
390            InvalidStateType::Overlap(idx1, idx2) => f.write_fmt(format_args!(
391                "car {idx1} and car {idx2} overlapped at position ({}, {})",
392                self.position.row, self.position.column
393            )),
394        }
395    }
396}
397
398impl<V> Error for InvalidStateError<V> where V: BoardValue {}
399
400fn add_car_concrete<V>(
401    board: &mut [Option<NonZeroUsize>],
402    idx: NonZeroUsize,
403    dim: &Dimensions<V>,
404    position: &Position<V>,
405    car: &Car<V>,
406) -> Result<(), InvalidStateError<V>>
407where
408    V: BoardValue,
409{
410    let mut base = *position;
411    let offset = match car.orientation {
412        Orientation::UpDown => Position {
413            row: V::one(),
414            column: V::zero(),
415        },
416        Orientation::LeftRight => Position {
417            row: V::zero(),
418            column: V::one(),
419        },
420    };
421    for _ in 0..car.length.into() {
422        match base.as_index(dim).and_then(|p| board.get_mut(p)) {
423            None => {
424                return Err(InvalidStateError {
425                    position: base,
426                    variant: InvalidStateType::InvalidPosition(idx),
427                });
428            }
429            Some(entry) => {
430                if let Some(existing) = entry {
431                    return Err(InvalidStateError {
432                        position: base,
433                        variant: InvalidStateType::Overlap(*existing, idx),
434                    });
435                }
436            }
437        }
438        base += offset;
439    }
440    let mut base = *position;
441    for _ in 0..car.length.into() {
442        board[base.as_index(dim).unwrap()] = Some(idx);
443        base += offset;
444    }
445    Ok(())
446}
447
448impl<V> State<V>
449where
450    V: BoardValue,
451{
452    fn concrete(&self) -> Result<Vec<Option<NonZeroUsize>>, InvalidStateError<V>> {
453        let mut board = vec![None; self.dim.columns.into() * self.dim.rows.into()];
454        for (idx, (position, car)) in self.cars.iter().enumerate() {
455            add_car_concrete(
456                &mut board,
457                NonZeroUsize::new(idx + 1).unwrap(),
458                &self.dim,
459                position,
460                car,
461            )?;
462        }
463        Ok(board)
464    }
465
466    /// An immutable representation of the current board, or an error if this state is invalid.
467    pub fn board(&self) -> Result<Board<&Self, V>, InvalidStateError<V>> {
468        Ok(Board {
469            concrete: self.concrete()?,
470            state: self,
471            phantom: PhantomData,
472        })
473    }
474
475    /// A mutable representation of the current board, or an error if this state is invalid.
476    pub fn board_mut(&mut self) -> Result<Board<&mut Self, V>, InvalidStateError<V>> {
477        Ok(Board {
478            concrete: self.concrete()?,
479            state: self,
480            phantom: PhantomData,
481        })
482    }
483}
484
485/// A concretised representation of the board.
486#[derive(Debug)]
487pub struct Board<R, V> {
488    state: R,
489    concrete: Vec<Option<NonZeroUsize>>,
490    phantom: PhantomData<V>,
491}
492
493impl<R, V> Hash for Board<R, V>
494where
495    R: Hash,
496{
497    fn hash<H: Hasher>(&self, state: &mut H) {
498        self.state.hash(state);
499    }
500}
501
502impl<R, V> Board<R, V> {
503    /// The [`Vec`] which represents the board literally.
504    pub fn concrete(&self) -> &Vec<Option<NonZeroUsize>> {
505        &self.concrete
506    }
507}
508
509impl<R, V> Board<R, V>
510where
511    R: Deref<Target = State<V>>,
512    V: BoardValue,
513{
514    /// Fetches the car index occupying the requested position. [`None`] if the position doesn't
515    /// exist in the board, [`Some`]`(`[`None`]`)` if the position exists, but is empty, and
516    /// [`Some`]`(`[`Some`]`(n))` with `n` as the car that occupies that position.
517    pub fn get<P: Into<Position<V>>>(&self, position: P) -> Option<Option<NonZeroUsize>> {
518        position
519            .into()
520            .as_index(&self.state.dim)
521            .and_then(|p| self.concrete.get(p).copied())
522    }
523}
524
525impl<R, V> Board<R, V>
526where
527    R: Deref<Target = State<V>>,
528{
529    /// Gets the current state of the board.
530    pub fn state(&self) -> &State<V> {
531        self.state.deref()
532    }
533}
534
535impl<R, V> Board<R, V>
536where
537    R: DerefMut<Target = State<V>>,
538{
539    /// Gets the current state of the board, mutably.
540    pub fn state_mut(&mut self) -> &mut State<V> {
541        self.state.deref_mut()
542    }
543}
544
545/// The type of invalid move that was observed in an [`InvalidMoveError`].
546#[derive(Debug)]
547pub enum InvalidMoveType<V> {
548    /// The car that was designated to be moved didn't exist.
549    InvalidCar,
550    /// The direction that was used isn't valid for the provided car.
551    InvalidDirection,
552    /// The final position of the car is out of bounds.
553    InvalidFinalPosition,
554    /// After moving the car, the car would intersect another with the provided index at the
555    /// provided position.
556    Intersects(Position<V>, NonZeroUsize),
557}
558
559/// An error which describes an attempted invalid move.
560#[derive(Debug)]
561pub struct InvalidMoveError<V> {
562    car: NonZeroUsize,
563    dir: Direction,
564    variant: InvalidMoveType<V>,
565}
566
567impl<V> Display for InvalidMoveError<V>
568where
569    V: BoardValue,
570{
571    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
572        match &self.variant {
573            InvalidMoveType::InvalidCar => f.write_fmt(format_args!("cannot move car {} {} because it doesn't exist", self.car, self.dir)),
574            InvalidMoveType::InvalidDirection => f.write_fmt(format_args!("cannot move car {} {} because its orientation does not allow for movement in that direction", self.car, self.dir)),
575            InvalidMoveType::InvalidFinalPosition => f.write_fmt(format_args!("cannot move car {} {} because it enters an invalid position", self.car, self.dir)),
576            InvalidMoveType::Intersects(pos, other) => f.write_fmt(format_args!("cannot move car {} {} because it would intersect with car {} at ({}, {})", self.car, self.dir, other, pos.row, pos.column)),
577        }
578    }
579}
580
581impl<V> Error for InvalidMoveError<V> where V: BoardValue {}
582
583impl<R, V> Board<R, V>
584where
585    R: DerefMut<Target = State<V>>,
586    V: BoardValue,
587{
588    /// Add a car to the board, updating the backing state in the process.
589    pub fn add_car<P: Into<Position<V>>>(
590        &mut self,
591        position: P,
592        car: Car<V>,
593    ) -> Result<NonZeroUsize, InvalidStateError<V>> {
594        let position = position.into();
595        let idx = NonZeroUsize::new(self.state.cars.len() + 1).unwrap();
596        add_car_concrete(&mut self.concrete, idx, &self.state.dim, &position, &car)?;
597        self.state.cars.push((position, car));
598        Ok(idx)
599    }
600}
601
602impl<R, V> Board<R, V>
603where
604    R: DerefMut<Target = State<V>>,
605    V: BoardValue,
606{
607    /// Shift a provided car one space in the designated direction.
608    pub fn shift_car(
609        &mut self,
610        car: NonZeroUsize,
611        dir: Direction,
612    ) -> Result<Position<V>, InvalidMoveError<V>> {
613        let idx = car.get() - 1;
614        if let Some((pos, actual)) = self.state.cars.get(idx).copied() {
615            let (deleted, inserted) = match (dir, actual.orientation) {
616                (Direction::Up, Orientation::UpDown)
617                | (Direction::Left, Orientation::LeftRight) => (
618                    pos.shift(-dir, actual.length - V::one()),
619                    pos.shift(dir, V::one()),
620                ),
621                (Direction::Down, Orientation::UpDown)
622                | (Direction::Right, Orientation::LeftRight) => {
623                    (Some(pos), pos.shift(dir, actual.length))
624                }
625                _ => {
626                    return Err(InvalidMoveError {
627                        car,
628                        dir,
629                        variant: InvalidMoveType::InvalidDirection,
630                    });
631                }
632            };
633            if let (Some(deleted_pos), Some(inserted_pos)) = (deleted, inserted) {
634                let deleted =
635                    deleted_pos
636                        .as_index(&self.state.dim)
637                        .ok_or_else(|| InvalidMoveError {
638                            car,
639                            dir,
640                            variant: InvalidMoveType::InvalidFinalPosition,
641                        })?;
642                let inserted =
643                    inserted_pos
644                        .as_index(&self.state.dim)
645                        .ok_or_else(|| InvalidMoveError {
646                            car,
647                            dir,
648                            variant: InvalidMoveType::InvalidFinalPosition,
649                        })?;
650                if let Ok([deleted, inserted]) = self.concrete.get_disjoint_mut([deleted, inserted])
651                {
652                    return if let Some(idx) = inserted {
653                        Err(InvalidMoveError {
654                            car,
655                            dir,
656                            variant: InvalidMoveType::Intersects(inserted_pos, *idx),
657                        })
658                    } else {
659                        *inserted = deleted.take();
660                        let new = pos.shift(dir, V::one()).unwrap();
661                        self.state.cars[idx].0 = new;
662                        Ok(new)
663                    };
664                }
665            }
666            Err(InvalidMoveError {
667                car,
668                dir,
669                variant: InvalidMoveType::InvalidFinalPosition,
670            })
671        } else {
672            Err(InvalidMoveError {
673                car,
674                dir,
675                variant: InvalidMoveType::InvalidCar,
676            })
677        }
678    }
679}
680
681impl<R, V> Display for Board<R, V>
682where
683    R: Deref<Target = State<V>>,
684    V: BoardValue,
685{
686    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
687        let num_width = (self.state.cars.len().saturating_sub(1).max(1).ilog10() + 2)
688            .next_multiple_of(2) as usize
689            - 1;
690        let side = num_width + 2;
691        let mut indices = self.concrete.iter().copied();
692        for _row in 0..self.state.dim.rows.into() {
693            for _padding in 0..(side / 2) {
694                writeln!(f, "{:#>1$}", "", self.state.dim.columns.into() * side)?;
695            }
696            for _column in 0..self.state.dim.columns.into() {
697                write!(f, "#")?;
698                if let Some(idx) = indices.next().unwrap() {
699                    write!(f, "{idx:0num_width$}")?;
700                } else {
701                    write!(f, "{:#>num_width$}", "")?;
702                }
703                write!(f, "#")?;
704            }
705            writeln!(f)?;
706            for _padding in 0..(side / 2) {
707                writeln!(f, "{:#>1$}", "", self.state.dim.columns.into() * side)?;
708            }
709        }
710        Ok(())
711    }
712}
713
714#[cfg(test)]
715mod test {
716    extern crate std;
717
718    use crate::{
719        Car, Direction, InvalidMoveError, InvalidMoveType, InvalidStateError, InvalidStateType,
720        Orientation, State,
721    };
722    use alloc::boxed::Box;
723    use core::error::Error;
724    use core::num::NonZeroUsize;
725    use std::println;
726
727    #[test]
728    fn simple_board() -> Result<(), Box<dyn Error>> {
729        let mut state = State::empty((2u8, 2))?;
730        let mut board = state.board_mut()?;
731        println!("{board}");
732        let idx = board.add_car((0, 0), Car::new(1, Orientation::LeftRight).unwrap())?;
733        assert_eq!(1, idx.get());
734        println!("{board}");
735        assert_eq!(1, board.get((0, 0)).unwrap().unwrap().get());
736
737        match board.add_car((2, 0), Car::new(1, Orientation::UpDown).unwrap()) {
738            Err(InvalidStateError {
739                position,
740                variant: InvalidStateType::InvalidPosition(idx),
741            }) => {
742                assert_eq!((2, 0), (position.row, position.column));
743                assert_eq!(2, idx.get());
744            }
745            _ => unreachable!("Should error here"),
746        }
747
748        match board.add_car((0, 0), Car::new(1, Orientation::UpDown).unwrap()) {
749            Err(InvalidStateError {
750                position,
751                variant: InvalidStateType::Overlap(idx1, idx2),
752            }) => {
753                assert_eq!((0, 0), (position.row, position.column));
754                assert_eq!(1, idx1.get());
755                assert_eq!(2, idx2.get());
756            }
757            _ => unreachable!("Should error here"),
758        }
759
760        board.shift_car(idx, Direction::Right)?;
761        assert_eq!(1, board.get((0, 1)).unwrap().unwrap().get());
762        assert_eq!(None, board.get((0, 0)).unwrap());
763        println!("{board}");
764
765        board.shift_car(idx, Direction::Left)?;
766        assert_eq!(1, board.get((0, 0)).unwrap().unwrap().get());
767        assert_eq!(None, board.get((0, 1)).unwrap());
768        println!("{board}");
769
770        let idx = board.add_car((0, 1), Car::new(1, Orientation::UpDown).unwrap())?;
771        assert_eq!(2, idx.get());
772        println!("{board}");
773        assert_eq!(2, board.get((0, 1)).unwrap().unwrap().get());
774
775        board.shift_car(idx, Direction::Down)?;
776        assert_eq!(2, board.get((1, 1)).unwrap().unwrap().get());
777        assert_eq!(None, board.get((0, 1)).unwrap());
778        println!("{board}");
779
780        board.shift_car(idx, Direction::Up)?;
781        assert_eq!(2, board.get((0, 1)).unwrap().unwrap().get());
782        assert_eq!(None, board.get((1, 1)).unwrap());
783        println!("{board}");
784
785        match board.shift_car(NonZeroUsize::new(3).unwrap(), Direction::Right) {
786            Err(InvalidMoveError {
787                variant: InvalidMoveType::InvalidCar,
788                car,
789                dir,
790            }) => {
791                assert_eq!(3, car.get());
792                assert_eq!(dir, Direction::Right)
793            }
794            s => unreachable!("Expected another error, got {s:?}"),
795        }
796
797        match board.shift_car(idx, Direction::Up) {
798            Err(InvalidMoveError {
799                variant: InvalidMoveType::InvalidFinalPosition,
800                car,
801                dir,
802            }) => {
803                assert_eq!(2, car.get());
804                assert_eq!(dir, Direction::Up)
805            }
806            s => unreachable!("Expected another error, got {s:?}"),
807        }
808
809        match board.shift_car(idx, Direction::Right) {
810            Err(InvalidMoveError {
811                variant: InvalidMoveType::InvalidDirection,
812                car,
813                dir,
814            }) => {
815                assert_eq!(2, car.get());
816                assert_eq!(dir, Direction::Right)
817            }
818            s => unreachable!("Expected another error, got {s:?}"),
819        }
820
821        match board.shift_car(NonZeroUsize::new(1).unwrap(), Direction::Right) {
822            Err(InvalidMoveError {
823                variant: InvalidMoveType::Intersects(at, with),
824                car,
825                dir,
826            }) => {
827                assert_eq!(1, car.get());
828                assert_eq!(2, with.get());
829                assert_eq!((0, 1), (at.row, at.column));
830                assert_eq!(dir, Direction::Right);
831            }
832            s => unreachable!("Expected another error, got {s:?}"),
833        }
834
835        let concrete = board.concrete().clone();
836        drop(board);
837
838        let board = state.board()?;
839
840        assert_eq!(&concrete, board.concrete());
841
842        Ok(())
843    }
844
845    #[test]
846    fn multi_board() -> Result<(), Box<dyn Error>> {
847        let mut state = State::empty((5u8, 5))?;
848        let mut board = state.board_mut()?;
849        println!("{board}");
850        let idx = board.add_car((0, 0), Car::new(3, Orientation::UpDown).unwrap())?;
851        assert_eq!(1, idx.get());
852        println!("{board}");
853
854        board.shift_car(idx, Direction::Down)?;
855        println!("{board}");
856
857        Ok(())
858    }
859}