Skip to main content

pinch_points/sim/
direction.rs

1/// A grid direction in screen space: row 0 is the top of the board, so `Up`
2/// decreases `y`. "Left of" and "right of" are from the walking agent's point
3/// of view (facing `Up`, the agent's left is `Left`).
4#[derive(Clone, Copy, PartialEq, Eq, Debug)]
5pub enum Direction {
6    Up,
7    Right,
8    Down,
9    Left,
10}
11
12impl Direction {
13    /// Every direction, clockwise from `Up`, for the callers that try all
14    /// four, so the array is not restated at each of them.
15    pub const ALL: [Direction; 4] = [
16        Direction::Up,
17        Direction::Right,
18        Direction::Down,
19        Direction::Left,
20    ];
21
22    /// The level-format letter (`U`/`D`/`L`/`R`); `from_letter` is its
23    /// inverse.
24    pub fn letter(self) -> char {
25        match self {
26            Direction::Up => 'U',
27            Direction::Down => 'D',
28            Direction::Left => 'L',
29            Direction::Right => 'R',
30        }
31    }
32
33    pub fn from_letter(letter: &str) -> Option<Direction> {
34        match letter {
35            "U" => Some(Direction::Up),
36            "D" => Some(Direction::Down),
37            "L" => Some(Direction::Left),
38            "R" => Some(Direction::Right),
39            _ => None,
40        }
41    }
42
43    /// The direction to the agent's left.
44    pub fn left(self) -> Self {
45        match self {
46            Direction::Up => Direction::Left,
47            Direction::Left => Direction::Down,
48            Direction::Down => Direction::Right,
49            Direction::Right => Direction::Up,
50        }
51    }
52
53    /// The direction to the agent's right.
54    pub fn right(self) -> Self {
55        match self {
56            Direction::Up => Direction::Right,
57            Direction::Right => Direction::Down,
58            Direction::Down => Direction::Left,
59            Direction::Left => Direction::Up,
60        }
61    }
62
63    pub fn reverse(self) -> Self {
64        match self {
65            Direction::Up => Direction::Down,
66            Direction::Down => Direction::Up,
67            Direction::Left => Direction::Right,
68            Direction::Right => Direction::Left,
69        }
70    }
71
72    /// Tile offset `(dx, dy)` of one step in this direction.
73    pub fn offset(self) -> (i32, i32) {
74        match self {
75            Direction::Up => (0, -1),
76            Direction::Down => (0, 1),
77            Direction::Left => (-1, 0),
78            Direction::Right => (1, 0),
79        }
80    }
81
82    pub(crate) fn id(self) -> u8 {
83        match self {
84            Direction::Up => 0,
85            Direction::Right => 1,
86            Direction::Down => 2,
87            Direction::Left => 3,
88        }
89    }
90
91    /// Inverse of [`Direction::id`]; masks to the low two bits, so any byte
92    /// decodes to some direction (wire formats rely on this).
93    pub(crate) fn from_id(id: u8) -> Direction {
94        match id & 0b11 {
95            0 => Direction::Up,
96            1 => Direction::Right,
97            2 => Direction::Down,
98            _ => Direction::Left,
99        }
100    }
101
102    /// Greedy direction of travel for a `(dx, dy)` displacement in screen
103    /// space: the axis with the larger magnitude wins, ties go horizontal.
104    /// `(0, 0)` yields `Left` by fall-through; callers exclude it.
105    pub fn toward(dx: i32, dy: i32) -> Direction {
106        if dx.abs() >= dy.abs() {
107            if dx > 0 {
108                Direction::Right
109            } else {
110                Direction::Left
111            }
112        } else if dy > 0 {
113            Direction::Down
114        } else {
115            Direction::Up
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::Direction;
123    use super::Direction::*;
124
125    #[test]
126    fn letters_round_trip_for_every_direction() {
127        for dir in [Up, Down, Left, Right] {
128            assert_eq!(Direction::from_letter(&dir.letter().to_string()), Some(dir));
129        }
130        assert_eq!(Direction::from_letter("X"), None);
131    }
132
133    #[test]
134    fn left_right_reverse_are_consistent() {
135        for d in [Up, Right, Down, Left] {
136            assert_eq!(d.left().right(), d);
137            assert_eq!(d.right().left(), d);
138            assert_eq!(d.left().left(), d.reverse());
139            assert_eq!(d.right().right(), d.reverse());
140            assert_eq!(d.reverse().reverse(), d);
141        }
142    }
143
144    #[test]
145    fn id_round_trips() {
146        for d in [Up, Right, Down, Left] {
147            assert_eq!(Direction::from_id(d.id()), d);
148        }
149        // Any byte decodes (wire safety).
150        for byte in 0..=u8::MAX {
151            let _ = Direction::from_id(byte);
152        }
153    }
154
155    #[test]
156    fn toward_picks_the_dominant_axis() {
157        assert_eq!(Direction::toward(5, 2), Right);
158        assert_eq!(Direction::toward(-5, 2), Left);
159        assert_eq!(Direction::toward(1, 4), Down);
160        assert_eq!(Direction::toward(1, -4), Up);
161        assert_eq!(Direction::toward(3, 3), Right); // tie goes horizontal
162        assert_eq!(Direction::toward(-3, -3), Left);
163    }
164
165    #[test]
166    fn screen_space_handedness() {
167        // Facing up the screen, the agent's left hand points to screen-left.
168        assert_eq!(Up.left(), Left);
169        assert_eq!(Down.left(), Right);
170    }
171}