Skip to main content

slidy/puzzle/
scrambler.rs

1//! Defines the [`Scrambler`] trait and several implementations.
2
3use rand::Rng;
4
5use crate::{
6    algorithm::{direction::Direction, r#move::r#move::Move},
7    puzzle::{size::Size, sliding_puzzle::SlidingPuzzle},
8};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Trait defining a scrambling algorithm.
14pub trait Scrambler {
15    /// Checks if this `Scrambler` can be used with a given puzzle size.
16    #[must_use]
17    fn is_valid_size(&self, size: Size) -> bool;
18
19    /// Equivalent to [`Scrambler::try_scramble_with_rng`] using [`rand::rng`].
20    #[cfg(feature = "thread_rng")]
21    fn try_scramble<P: SlidingPuzzle>(&self, puzzle: &mut P) -> bool {
22        self.try_scramble_with_rng(puzzle, &mut rand::rng())
23    }
24
25    /// Equivalent to [`Scrambler::scramble_with_rng`] using [`rand::rng`].
26    #[cfg(feature = "thread_rng")]
27    fn scramble<P: SlidingPuzzle>(&self, puzzle: &mut P) {
28        self.scramble_with_rng(puzzle, &mut rand::rng());
29    }
30
31    /// Scrambles the puzzle using a given [`Rng`]. If the puzzle is not of a valid size for the
32    /// scrambler, the function returns false and the puzzle is not modified.
33    fn try_scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R) -> bool {
34        if self.is_valid_size(puzzle.size()) {
35            self.scramble_with_rng(puzzle, rng);
36            true
37        } else {
38            false
39        }
40    }
41
42    /// See [`Scrambler::try_scramble_with_rng`].
43    ///
44    /// This function may not check whether the puzzle is of a valid size for the scrambler. If it
45    /// is not, then the function may panic or scramble the puzzle into an unsolvable or invalid
46    /// state.
47    fn scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R);
48}
49
50/// Random state scrambler, but leaving the gap in the bottom right corner so that the resulting
51/// state is invertible.
52///
53/// See [`RandomState`].
54#[derive(Clone, Default, Debug, PartialEq, Eq)]
55#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
56pub struct RandomInvertibleState;
57
58impl Scrambler for RandomInvertibleState {
59    fn is_valid_size(&self, size: Size) -> bool {
60        size.width() > 1 && size.height() > 1
61    }
62
63    fn scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R) {
64        puzzle.reset();
65
66        let n = puzzle.num_pieces();
67        let mut parity = false;
68        for i in 0..n - 2 {
69            // Pick random element to go in position i
70            let j = rng.random_range(i..n);
71
72            // Swap and check if we need to toggle parity
73            if i != j {
74                puzzle.swap_non_gap_pieces(i, j);
75                parity = !parity;
76            }
77        }
78
79        // Swap the last two pieces if necessary to make it solvable
80        if parity {
81            puzzle.swap_non_gap_pieces(n - 2, n - 1);
82        }
83    }
84}
85
86/// Random state scrambler. Scrambles the puzzle in such a way that every solvable state is equally
87/// likely to occur.
88#[derive(Clone, Default, Debug, PartialEq, Eq)]
89#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
90pub struct RandomState;
91
92impl Scrambler for RandomState {
93    fn is_valid_size(&self, _size: Size) -> bool {
94        true
95    }
96
97    fn scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R) {
98        let (w, h) = puzzle.size().into();
99
100        if w == 1 {
101            puzzle.reset();
102            let d = rng.random_range(0..h);
103            puzzle.apply_move(Move::new(Direction::Down, d));
104            return;
105        }
106
107        if h == 1 {
108            puzzle.reset();
109            let r = rng.random_range(0..w);
110            puzzle.apply_move(Move::new(Direction::Right, r));
111            return;
112        }
113
114        RandomInvertibleState.scramble_with_rng(puzzle, rng);
115
116        // Move blank to a random position
117        let (d, r) = (rng.random_range(0..h), rng.random_range(0..w));
118
119        puzzle.apply_move(Move::new(Direction::Down, d));
120        puzzle.apply_move(Move::new(Direction::Right, r));
121    }
122}
123
124/// Scrambles the puzzle by applying a fixed number of random single-tile moves.
125#[derive(Clone, Debug, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
127pub struct RandomMoves {
128    /// Number of random moves to apply.
129    pub moves: u64,
130    /// Are backtracking moves allowed? E.g. If one move of the scramble is R, is the next move
131    /// allowed to be L? If this is false, the L move will not be allowed and a differentmove will
132    /// be generated.
133    pub allow_backtracking: bool,
134    /// Are illegal moves counted? E.g. If the first generated move of the scramble is L (which
135    /// can not be applied to the puzzle), should this be counted towards the total move count? If
136    /// this is false, the L move will not be counted and a different move will be generated.
137    pub allow_illegal_moves: bool,
138}
139
140impl Scrambler for RandomMoves {
141    fn is_valid_size(&self, size: Size) -> bool {
142        // If the puzzle is 1xn or nx1 and we don't allow backtracking or illegal moves, then after
143        // n-1 moves, there will be no legal moves, and the `while` loop in `scramble_with_rng`
144        // would loop forever.
145        if (size.width() == 1 || size.height() == 1)
146            && !self.allow_backtracking
147            && !self.allow_illegal_moves
148        {
149            self.moves < size.area()
150        } else {
151            true
152        }
153    }
154
155    fn scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R) {
156        puzzle.reset();
157
158        let mut last_dir = None::<Direction>;
159        for _ in 0..self.moves {
160            let dir = {
161                let mut d = rng.random::<Direction>();
162                while (!self.allow_backtracking && last_dir == Some(d.inverse()))
163                    || (!self.allow_illegal_moves && !puzzle.can_move_dir(d))
164                {
165                    d = rng.random();
166                }
167                d
168            };
169
170            last_dir = Some(dir);
171            puzzle.try_move_dir(dir);
172        }
173    }
174}
175
176/// Scrambler that applies a single cycle of pieces to the puzzle. If `length` is even, the last
177/// two pieces in the puzzle will also be swapped to make it solvable.
178#[derive(Clone, Debug, PartialEq, Eq)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180pub struct Cycle {
181    /// Length of the cycle.
182    pub length: u64,
183}
184
185impl Scrambler for Cycle {
186    fn is_valid_size(&self, size: Size) -> bool {
187        // We can't do any cycles on a 1xn or nx1 puzzle.
188        size.width() > 1 && size.height() > 1
189    }
190
191    fn scramble_with_rng<P: SlidingPuzzle, R: Rng>(&self, puzzle: &mut P, rng: &mut R) {
192        puzzle.reset();
193
194        let n = puzzle.num_pieces();
195        let cycle_len = (self.length).min(if n.is_multiple_of(2) { n - 1 } else { n });
196        let max = if cycle_len % 2 == 0 { n - 2 } else { n };
197        let pieces = rand::seq::index::sample(rng, max as usize, cycle_len as usize);
198
199        for i in 1..cycle_len as usize {
200            puzzle.try_swap_pieces(pieces.index(0) as u64, pieces.index(i) as u64);
201        }
202
203        if self.length.is_multiple_of(2) {
204            puzzle.try_swap_pieces(n - 2, n - 1);
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use crate::puzzle::puzzle::Puzzle;
212
213    use super::*;
214
215    mod random_invertible_state {
216        use rand::SeedableRng as _;
217        use rand_xoshiro::Xoroshiro128StarStar;
218
219        use super::*;
220
221        const SEED: [u8; 16] = [
222            160, 108, 126, 255, 147, 210, 122, 252, 71, 77, 144, 13, 167, 11, 225, 93,
223        ];
224
225        #[test]
226        fn test_gap_in_bottom_right() {
227            let mut rng = Xoroshiro128StarStar::from_seed(SEED);
228
229            for s in 2..10 {
230                let mut p = Puzzle::new(Size::new(s, s).unwrap());
231
232                for _ in 0..100 {
233                    RandomInvertibleState.scramble_with_rng(&mut p, &mut rng);
234                    assert_eq!(p.gap_position_xy(), (s - 1, s - 1));
235                }
236            }
237        }
238    }
239
240    mod random_state {
241        use rand::SeedableRng as _;
242        use rand_xoshiro::Xoroshiro128StarStar;
243
244        use crate::puzzle::{label::label::RowGrids, solvable::Solvable as _};
245
246        use super::*;
247
248        const SEED: [u8; 16] = [
249            160, 108, 126, 255, 147, 210, 122, 252, 71, 77, 144, 13, 167, 11, 225, 93,
250        ];
251
252        #[test]
253        fn test_solvable() {
254            let mut rng = Xoroshiro128StarStar::from_seed(SEED);
255
256            for (w, h) in [(1, 1), (1, 4), (4, 1), (2, 2), (4, 4), (10, 2), (20, 20)] {
257                let mut p = Puzzle::new(Size::new(w, h).unwrap());
258
259                for _ in 0..100 {
260                    RandomState.scramble_with_rng(&mut p, &mut rng);
261                    assert!(RowGrids.is_solvable(&p));
262                }
263            }
264        }
265    }
266}
267
268#[cfg(all(feature = "nightly", test))]
269mod benchmarks {
270    extern crate test;
271
272    use rand::SeedableRng;
273    use rand_xoshiro::Xoroshiro128StarStar;
274    use test::Bencher;
275
276    use crate::puzzle::puzzle::Puzzle;
277
278    use super::*;
279
280    #[bench]
281    fn bench_random_state(b: &mut Bencher) {
282        let mut p = Puzzle::new(Size::new(100, 100).unwrap());
283        let mut rng = Xoroshiro128StarStar::seed_from_u64(0);
284
285        b.iter(|| RandomState.scramble_with_rng(&mut p, &mut rng));
286    }
287}