Skip to main content

falling_tetromino_engine/
rotation.rs

1/*!
2This module handles rotation of [`Piece`]s.
3*/
4
5use crate::{Board, Orientation, Piece, Tetromino};
6
7/// Handles the logic of how to rotate a tetromino in play.
8#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash, Debug, Default)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum RotationSystem {
11    /// The raw rotation system resulting from simply reorienting the internal piece representation.
12    /// This can be seen as a debug implementation.
13    /// The rotation itself looks similar to [`RotationSystem::ClassicL`].
14    Raw,
15    /// The 'Ocular' rotation system.
16    #[default]
17    Ocular,
18    /// The left-handed variant of the classic, kick-less rotation system, e.g. used in the Gameboy version.
19    ClassicL,
20    /// The right-handed variant of the classic, kick-less rotation system, e.g. used in the NES version.
21    ClassicR,
22    /// The Super Rotation System.
23    Super,
24}
25
26impl RotationSystem {
27    /// Tries to rotate a piece with the chosen `RotationSystem`.
28    ///
29    /// This will return `Ok(new_piece)` if the `old_piece`, when rotated
30    /// `right_turns`-times from its position, fit onto the board, ending up as `new_piece`;
31    /// It will return None if not.
32    ///
33    /// In particular, rotating a piece `0` times tests whether piece fits in its *current* position.
34    pub fn rotate(&self, piece: &Piece, board: &Board, right_turns: i8) -> Option<Piece> {
35        match self {
36            RotationSystem::Raw => raw_rotate(piece, board, right_turns),
37            RotationSystem::Ocular => ocular_rotate(piece, board, right_turns),
38            RotationSystem::ClassicL => classic_rotate(piece, board, right_turns, false),
39            RotationSystem::ClassicR => classic_rotate(piece, board, right_turns, true),
40            RotationSystem::Super => super_rotate(piece, board, right_turns),
41        }
42    }
43}
44
45fn raw_rotate(piece: &Piece, board: &Board, right_turns: i8) -> Option<Piece> {
46    piece.reoriented_offset_on(board, right_turns, (0, 0)).ok()
47}
48
49/*
50The basic ideas of Ocular Rotation are not that hard:
511. Use symmetry for kick tables (e.g. O↻ := ⇔O↺).
522. For the remaining, unique entries, list out kicks that look intuitive or desirable.
53
54Rotation Symmetries to figure out kicks from existing kicks:
55    Notation:
56        OISZTLJ   (↑→↓←)         ↺↻
57        ^shapes.  ^orientation.  ^rotation direction.
58    And "⇔" means "mirrored horizontally".
59
60Given we know how  O↺  then we can figure out [the rest of O]:
61         O↻  :=  ⇔ O↺
62
63Given we know how  I(↑→)↺  then we can figure out [the rest of I]:
64     I(↑→)↻  :=  ⇔ I(↑→)↺
65
66Given we know how  S(↑→)↺↻  then we can figure out [all of Z]:
67    Z(↑→)↺↻  :=  ⇔ S(↑→)↻↺
68
69Given we know how  T(↑→↓←)↺  then we can figure out [the rest of T]:
70     T(↑↓)↻  :=  ⇔ T(↑↓)↺
71     T(→←)↻  :=  ⇔ T(←→)↺
72
73Given we know how  L(↑→↓←)↺↻  then we can figure out [all of J]:
74    J(↑↓)↺↻  :=  ⇔ L(↑↓)↻↺"
75    J(→←)↺↻  :=  ⇔ L(←→)↻↺"
76*/
77#[rustfmt::skip]
78fn ocular_rotate(piece: &Piece, board: &Board, right_turns: i8) -> Option<Piece> {
79    use Orientation::*;
80    // Figure out whether to turn 'right' (90° CW), 'left' (90° CCW), 'around' (180°) or not at all (0°).
81    match right_turns.rem_euclid(4) {
82        // 0° - "Rotate into same orientation".
83        0 => piece.offset_on(board, (0, 0)).ok(),
84
85        // 180° - Rotate 'around'.
86        2 => {
87            let mut lookup_tetromino = piece.tetromino;
88            let mut lookup_orientation = piece.orientation;
89            let mut apply_mirror = false;
90            // Precompute mirror / horizontal reorientation to possibly change lookup_orientation once (see T, J).
91            let reorient_horizontally = match piece.orientation { N => N, E => W, S => S, W => E };
92
93            let kick_table = 'lookup: loop {
94                break match lookup_tetromino {
95                    
96                    // Note: O and I have a default, successful 180° rotation due to 180° symmetry.
97                    Tetromino::O | Tetromino::I => &[( 0, 0)][..],
98                    
99                    // Note: S has special 180° rotations that can 'nudge' it diagonally into fitting gaps.
100                    // Note: S has a default, successful 180° rotation due to 180° symmetry.
101                    Tetromino::S => match lookup_orientation {
102                        N | S => &[(-1,-1), ( 0, 0)][..],
103                        E | W => &[( 1,-1), ( 0, 0)][..],
104                    },
105
106                    Tetromino::Z => {
107                        // Symmetry: Z's 180° rotation is a mirrored version of S'.
108                        lookup_tetromino = Tetromino::S;
109                        apply_mirror = true;
110                        continue 'lookup;
111                    },
112                    
113                    Tetromino::T => match lookup_orientation {
114                        N => &[( 0,-1), ( 0, 0)][..],
115                        E => &[(-1, 0), ( 0, 0), (-1,-1)][..],
116                        S => &[( 0, 1), ( 0, 0), ( 0,-1)][..],
117                        W => {
118                             // Symmetry: T's 180° rotation oriented West is same as mirrored East.
119                            lookup_orientation = reorient_horizontally;
120                            apply_mirror = true;
121                            continue 'lookup;
122                        },
123                    },
124
125                    Tetromino::L => match lookup_orientation {
126                        N => &[( 0,-1), ( 1,-1), (-1,-1), ( 0, 0), ( 1, 0)][..],
127                        E => &[(-1, 0), (-1,-1), ( 0, 0), ( 0,-1)][..],
128                        S => &[( 0, 1), ( 0, 0), (-1, 1), (-1, 0)][..],
129                        W => &[( 1, 0), ( 0, 0), ( 1,-1), ( 1, 1), ( 0, 1)][..],
130                    },
131                    
132                    Tetromino::J => {
133                        // Symmetry: J's 180° rotation is a mirrored version of L's.
134                        lookup_tetromino = Tetromino::L;
135                        lookup_orientation = reorient_horizontally;
136                        apply_mirror = true;
137                        continue 'lookup;
138                    }
139                }
140            };
141
142            // Mirror kicks in case we used symmetry to figure out what to do.
143            let offsets = kick_table.iter().copied().map(|(x, y)| if apply_mirror { (-x, y) } else { (x, y) });
144            // Using kick table, actually find whether piece fits at a given place.
145            piece.find_reoriented_offset_on(board, right_turns, offsets)
146        }
147
148        // ± 90° - Rotate 'right'/'left'.
149        rot => {
150            // `rot` at this point can only be 1 ('right') or 3 ('left').
151            let mut lookup_leftrot = rot == 3;
152            let mut lookup_tetromino = piece.tetromino;
153            let mut lookup_orientation = piece.orientation;
154            // Unlike 180°, mirroring a piece may involve adding a manual offset to make it look symmetric as desired.
155            let mut apply_mirror = None;
156            // Precompute mirror / horizontal reorientation to possibly change lookup_orientation once (see T, J).
157            let reorient_horizontally = match lookup_orientation { N => N, E => W, S => S, W => E };
158
159            let kick_table = 'lookup: loop {
160                match lookup_tetromino {
161                    Tetromino::O => {
162                        if lookup_leftrot {
163                            break 'lookup &[(-1, 0), (-1,-1), (-1, 1), ( 0, 0)][..];
164                        } else  {
165                            // Symmetry: O's right rotation is a mirrored version of left rotation.
166                            apply_mirror = Some(0);
167                            lookup_leftrot = true;
168                            continue 'lookup;
169                        }
170                    },
171
172                    Tetromino::I => {
173                        if lookup_leftrot {
174                            break 'lookup match lookup_orientation {
175                                N | S => &[( 1,-1), ( 1,-2), ( 1,-3), ( 0,-1), ( 0,-2), ( 0,-3), ( 1, 0), ( 0, 0), ( 2,-1), ( 2,-2)][..],
176                                E | W => &[(-2, 1), (-3, 1), (-2, 0), (-3, 0), (-1, 1), (-1, 0), ( 0, 1), ( 0, 0)][..],
177                            };
178                        } else  {
179                            // Symmetry: I's right rotation is a mirrored version of left rotation.
180                            // (Manual x offset due to how engine naïvely positions base shapes.)
181                            let dx = match lookup_orientation { N | S => 3, E | W => -3 };
182                            apply_mirror = Some(dx);
183                            lookup_leftrot = true;
184                            continue 'lookup;
185                        }
186                    },
187
188                    Tetromino::S => break 'lookup match lookup_orientation {
189                        N | S => if lookup_leftrot { &[( 0, 0), ( 0,-1), ( 1, 0), (-1,-1)][..] }
190                                              else { &[( 1, 0), ( 1,-1), ( 1, 1), ( 0, 0), ( 0,-1)][..] },
191                        E | W => if lookup_leftrot { &[(-1, 0), ( 0, 0), (-1,-1), (-1, 1), ( 0, 1)][..] }
192                                              else { &[( 0, 0), (-1, 0), ( 0,-1), ( 1, 0), ( 0, 1), (-1, 1)][..] },
193                    },
194
195                    Tetromino::Z => {
196                        // Symmetry: Z's left/right rotation is a mirrored version of S' right/left rotation.
197                        // (Manual x offset due to how engine naïvely positions base shapes.)
198                        let dx = match lookup_orientation { N | S => 1, E | W => -1 };
199                        apply_mirror = Some(dx);
200                        lookup_tetromino = Tetromino::S;
201                        lookup_leftrot = !lookup_leftrot;
202                        continue 'lookup;
203                    },
204
205                    Tetromino::T => {
206                        if lookup_leftrot {
207                            break 'lookup match lookup_orientation {
208                                N => &[( 0,-1), ( 0, 0), (-1,-1), ( 1,-1), (-1,-2), ( 1, 0)][..],
209                                E => &[(-1, 1), (-1, 0), ( 0, 1), ( 0, 0), (-1,-1), (-1, 2)][..],
210                                S => &[( 1, 0), ( 0, 0), ( 1,-1), ( 0,-1), ( 1,-2), ( 2, 0)][..],
211                                W => &[( 0, 0), (-1, 0), ( 0,-1), (-1,-1), ( 1,-1), ( 0, 1), (-1, 1)][..],
212                            };
213                        } else  {
214                            // Symmetry: T's right rotation is a mirrored version of left rotation if reoriented.
215                            let dx = match lookup_orientation { N | S => 1, E | W => -1 };
216                            apply_mirror = Some(dx);
217                            lookup_orientation = reorient_horizontally;
218                            lookup_leftrot = true;
219                            continue 'lookup;
220                        }
221                    },
222
223                    Tetromino::L => break match lookup_orientation {
224                        N => if lookup_leftrot { &[( 0,-1), ( 1,-1), ( 0,-2), ( 1,-2), ( 0, 0), ( 1, 0)][..] }
225                                          else { &[( 1,-1), ( 1, 0), ( 1,-1), ( 2, 0), ( 0,-1), ( 0, 0)][..] },
226                        E => if lookup_leftrot { &[(-1, 1), (-1, 0), (-2, 1), (-2, 0), ( 0, 0), ( 0, 1)][..] }
227                                          else { &[(-1, 0), ( 0, 0), ( 0,-1), (-1,-1), ( 0, 1), (-1, 1)][..] },
228                        S => if lookup_leftrot { &[( 1, 0), ( 0, 0), ( 1,-1), ( 0,-1), ( 0, 1), ( 1, 1)][..] }
229                                          else { &[( 0, 0), ( 0,-1), ( 1,-1), (-1,-1), ( 1, 0), (-1, 0), ( 0, 1)][..] },
230                        W => if lookup_leftrot { &[( 0, 0), (-1, 0), ( 0, 1), ( 1, 0), (-1, 1), ( 1, 1), ( 0,-1), (-1,-1)][..] }
231                                          else { &[( 0, 1), (-1, 1), ( 0, 0), (-1, 0), ( 0, 2), (-1, 2)][..] },
232                    },
233
234                    Tetromino::J => {
235                        // Symmetry: J's left/right rotation is a mirrored version of L's right/left rotation if reoriented.
236                        let dx = match lookup_orientation { N | S => 1, E | W => -1 };
237                        apply_mirror = Some(dx);
238                        lookup_tetromino = Tetromino::L;
239                        lookup_orientation = reorient_horizontally;
240                        lookup_leftrot = !lookup_leftrot;
241                        continue 'lookup;
242                    }
243                }
244            };
245
246            // Mirror kicks in case we used symmetry to figure out what to do.
247            let offsets = kick_table.iter().copied().map(|(x, y)| if let Some(mx) = apply_mirror { (mx - x, y) } else { (x, y) });
248            // Using kick table, actually find whether piece fits at a given place.
249            piece.find_reoriented_offset_on(board, right_turns, offsets)
250        },
251    }
252}
253
254fn classic_rotate(
255    piece: &Piece,
256    board: &Board,
257    right_turns: i8,
258    is_r_not_l: bool,
259) -> Option<Piece> {
260    let r_variant_offset = if is_r_not_l { 1 } else { 0 };
261    #[rustfmt::skip]
262    let kick = match right_turns.rem_euclid(4) {
263        // "Rotate into same orientation".
264        0 => (0, 0),
265        // Classic didn't define 180 rotation, just check if the "default" 180 rotation fits.
266        2 => {
267            use Orientation::*;
268            match piece.tetromino {
269                Tetromino::O | Tetromino::I | Tetromino::S | Tetromino::Z => (0, 0),
270                Tetromino::T | Tetromino::L | Tetromino::J => match piece.orientation {
271                    N => (0, -1),
272                    S => (0, 1),
273                    E => (-1, 0),
274                    W => (1, 0),
275                },
276            }
277        }
278        // One right rotation.
279        r => {
280            use Orientation::*;
281            match piece.tetromino {
282                Tetromino::O => (0, 0), // ⠶
283                Tetromino::I => match piece.orientation {
284                    N | S => (1+r_variant_offset, -1), // ⠤⠤ -> ⡇
285                    E | W => (-1-r_variant_offset, 1), // ⡇  -> ⠤⠤
286                },
287                Tetromino::S | Tetromino::Z => match piece.orientation {
288                    N | S => (r_variant_offset, 0),  // ⠴⠂ -> ⠳  // ⠲⠄ -> ⠞
289                    E | W => (-r_variant_offset, 0), // ⠳  -> ⠴⠂ // ⠞  -> ⠲⠄
290                },
291                Tetromino::T | Tetromino::L | Tetromino::J => match piece.orientation {
292                    N => if r == 3 { ( 0,-1) } else { ( 1,-1) }, // ⠺  <- ⠴⠄ -> ⠗  // ⠹  <- ⠤⠆ -> ⠧  // ⠼  <- ⠦⠄ -> ⠏
293                    E => if r == 3 { (-1, 1) } else { (-1, 0) }, // ⠴⠄ <- ⠗  -> ⠲⠂ // ⠤⠆ <- ⠧  -> ⠖⠂ // ⠦⠄ <- ⠏  -> ⠒⠆
294                    S => if r == 3 { ( 1, 0) } else { ( 0, 0) }, // ⠗  <- ⠲⠂ -> ⠺  // ⠧  <- ⠖⠂ -> ⠹  // ⠏  <- ⠒⠆ -> ⠼
295                    W => if r == 3 { ( 0, 0) } else { ( 0, 1) }, // ⠲⠂ <- ⠺  -> ⠴⠄ // ⠖⠂ <- ⠹  -> ⠤⠆ // ⠒⠆ <- ⠼  -> ⠦⠄
296                },
297            }
298        },
299    };
300
301    piece.reoriented_offset_on(board, right_turns, kick).ok()
302}
303
304fn super_rotate(piece: &Piece, board: &Board, right_turns: i8) -> Option<Piece> {
305    let left = match right_turns.rem_euclid(4) {
306        // "Rotate into same orientation".
307        0 => return piece.offset_on(board, (0, 0)).ok(),
308        // One right rotation.
309        1 => false,
310        // Some basic 180 rotation I came up with.
311        2 => {
312            #[rustfmt::skip]
313            let kick_table = match piece.tetromino {
314                Tetromino::O | Tetromino::I | Tetromino::S | Tetromino::Z => &[(0, 0)][..],
315                Tetromino::T | Tetromino::L | Tetromino::J => match piece.orientation {
316                    N => &[( 0,-1), ( 0, 0)][..],
317                    E => &[(-1, 0), ( 0, 0)][..],
318                    S => &[( 0, 1), ( 0, 0)][..],
319                    W => &[( 1, 0), ( 0, 0)][..],
320                },
321            };
322            return piece.find_reoriented_offset_on(board, 2, kick_table.iter().copied());
323        }
324        // One left rotation.
325        3 => true,
326        _ => unreachable!(),
327    };
328    use Orientation::*;
329    #[rustfmt::skip]
330    let kick_table = match piece.tetromino {
331        Tetromino::O => &[(0, 0)][..],
332        Tetromino::I => match piece.orientation {
333            N => if left { &[( 1,-2), ( 0,-2), ( 3,-2), ( 0, 0), ( 3,-3)][..] }
334                    else { &[( 2,-2), ( 0,-2), ( 3,-2), ( 0,-3), ( 3, 0)][..] },
335            E => if left { &[(-2, 2), ( 0, 2), (-3, 2), ( 0, 3), (-3, 0)][..] }
336                    else { &[(-2, 1), (-3, 1), ( 0, 1), (-3, 3), ( 0, 0)][..] },
337            S => if left { &[( 2,-1), ( 3,-1), ( 0,-1), ( 3,-3), ( 0, 0)][..] }
338                    else { &[( 1,-1), ( 3,-1), ( 0,-1), ( 3, 0), ( 0,-3)][..] },
339            W => if left { &[(-1, 1), (-3, 1), ( 0, 1), (-3, 0), ( 0, 3)][..] }
340                    else { &[(-1, 2), ( 0, 2), (-3, 2), ( 0, 0), (-3, 3)][..] },
341        },
342        Tetromino::S | Tetromino::Z | Tetromino::T | Tetromino::L | Tetromino::J => match piece.orientation {
343            N => if left { &[( 0,-1), ( 1,-1), ( 1, 0), ( 0,-3), ( 1,-3)][..] }
344                    else { &[( 1,-1), ( 0,-1), ( 0, 0), ( 1,-3), ( 0,-3)][..] },
345            E => if left { &[(-1, 1), ( 0, 1), ( 0, 0), (-1, 3), ( 0, 3)][..] }
346                    else { &[(-1, 0), ( 0, 0), ( 0,-1), (-1, 2), ( 0, 2)][..] },
347            S => if left { &[( 1, 0), ( 0, 0), (-1, 1), ( 1,-2), ( 0,-2)][..] }
348                    else { &[( 0, 0), ( 1, 0), ( 1, 1), ( 0,-2), ( 1,-2)][..] },
349            W => if left { &[( 0, 0), (-1, 0), (-1,-1), ( 0, 2), (-1, 2)][..] }
350                    else { &[( 0, 1), (-1, 1), (-1, 0), ( 0, 3), (-1, 3)][..] },
351        },
352    };
353    piece.find_reoriented_offset_on(board, right_turns, kick_table.iter().copied())
354}