hex_spiral/
lib.rs

1//! While most hex-grid-based 2D games use multiple coordinates, **hex-spiral** uses a single-coordinate spiral,
2//! where the central hex has the position `0`, and further hexes are placed within theoretical hexagonal rings
3//! that surround it. The hexes are flat-topped and every ring is indexed starting with the hex on the top edge
4//! of the previous ring and with further positions growing clockwise.
5
6use itertools::Itertools;
7
8pub type Pos = usize;
9pub type RingIdx = usize;
10
11crepe::crepe! {
12    @input
13    struct Position(Pos);
14
15    struct Neighbors(Pos, Pos);
16
17    @output
18    struct Grouped(Pos, Pos);
19
20    Neighbors(p1, p2) <- Position(p1), Position(p2), (are_neighbors(p1, p2));
21    Grouped(p1, p2) <- Neighbors(p1, p2);
22    Grouped(p1, p3) <- Neighbors(p1, p2), Grouped(p2, p3), (p1 != p3);
23}
24
25/// The starting position of hexes within the ring with the given index.
26pub fn ring_offset(ring: RingIdx) -> Pos {
27    if ring == 0 {
28        0
29    } else {
30        3 * (ring - 1) * ring + 1
31    }
32}
33
34/// The index of the ring for the given position.
35pub fn ring(pos: Pos) -> RingIdx {
36    (0..)
37        .map(ring_offset)
38        .position(|offset| pos < offset)
39        .unwrap()
40        - 1
41}
42
43/// Returns `true` if the given position is at one of the tips of a ring.
44pub fn is_at_ring_tip(pos: Pos) -> bool {
45    let ring = ring(pos);
46    let ring_offset = ring_offset(ring);
47
48    (0..6)
49        .map(|n| ring_offset + n * ring)
50        .any(|ring_tip| pos == ring_tip)
51}
52
53/// Returns the index of the edge of the ring the given position belongs to.
54pub fn ring_edge_index(pos: Pos) -> usize {
55    let ring = ring(pos);
56    (pos - ring_offset(ring)) / ring
57}
58
59/// An iterator returning subsequent neighboring positions in the given direction.
60/// The top direction is `0`, and it increases up to `5` clockwise.
61pub struct DirectionalNeighborIter {
62    curr_pos: Pos,
63    dir: usize,
64}
65
66impl DirectionalNeighborIter {
67    /// Create a new `DirectionalNeighborIter` starting at the given position
68    /// and progressing in the chosen direction.
69    pub fn new(pos: Pos, dir: usize) -> Self {
70        assert!(dir <= 5);
71        Self { curr_pos: pos, dir }
72    }
73
74    /// Returns the position the `DirectionalNeighborIter` is currently at.
75    pub fn curr_pos(&self) -> Pos {
76        self.curr_pos
77    }
78}
79
80impl Iterator for DirectionalNeighborIter {
81    type Item = Pos;
82
83    fn next(&mut self) -> Option<Self::Item> {
84        let next = neighboring_positions(self.curr_pos)[self.dir];
85        self.curr_pos = next;
86        Some(next)
87    }
88}
89
90fn ring_neighboring_positions(pos: Pos) -> [Pos; 2] {
91    assert!(pos != 0);
92
93    let ring = ring(pos);
94
95    if pos == ring_offset(ring) {
96        [ring_offset(ring + 1) - 1, pos + 1]
97    } else if pos == ring_offset(ring + 1) - 1 {
98        [pos - 1, ring_offset(ring)]
99    } else {
100        [pos - 1, pos + 1]
101    }
102}
103
104/// Returns the 6 neighbors of the given position, always in the same clockwise order.
105pub fn neighboring_positions(pos: Pos) -> [Pos; 6] {
106    let ring = ring(pos);
107
108    match ring {
109        0 => [1, 2, 3, 4, 5, 6],
110        _ => {
111            let edge_index = ring_edge_index(pos);
112
113            let mut poss = if is_at_ring_tip(pos) {
114                // 1 neighbor from the lower ring, 3 from the upper ring, 2 from the same ring
115                let lower_neighbor = ring_offset(ring - 1) + (ring - 1) * edge_index;
116                let ring_neighbors = ring_neighboring_positions(pos);
117                let upper_tip_neighbor = if pos == ring_offset(ring + 1) - 1 {
118                    ring_offset(ring + 2) - 2
119                } else {
120                    ring_offset(ring + 1) + (ring + 1) * edge_index
121                };
122                let upper_tip_neighbors = ring_neighboring_positions(upper_tip_neighbor);
123
124                [
125                    upper_tip_neighbor,
126                    upper_tip_neighbors[1],
127                    ring_neighbors[1],
128                    lower_neighbor,
129                    ring_neighbors[0],
130                    upper_tip_neighbors[0],
131                ]
132            } else {
133                // 2 neighbors from the lower ring, the upper ring, and the same ring
134                let ring_pos = pos - ring_offset(ring);
135                let tip_offset = ring_pos - (edge_index * ring);
136                let (lower_neighbor1, lower_neighbor2) = if pos == ring_offset(ring + 1) - 1 {
137                    (ring_offset(ring) - 1, ring_offset(ring - 1))
138                } else {
139                    let lower_neighbor1 =
140                        ring_offset(ring - 1) + (edge_index * (ring - 1)) + tip_offset - 1;
141                    (lower_neighbor1, lower_neighbor1 + 1)
142                };
143                let ring_neighbors = ring_neighboring_positions(pos);
144                let upper_neighbor1 =
145                    ring_offset(ring + 1) + (edge_index * (ring + 1)) + tip_offset;
146                let upper_neighbor2 = upper_neighbor1 + 1;
147
148                [
149                    upper_neighbor1,
150                    upper_neighbor2,
151                    ring_neighbors[1],
152                    lower_neighbor2,
153                    lower_neighbor1,
154                    ring_neighbors[0],
155                ]
156            };
157
158            poss.rotate_right(edge_index);
159
160            poss
161        }
162    }
163}
164
165/// Returns `true` if the given 2 positions are neighbors.
166pub fn are_neighbors(pos1: Pos, pos2: Pos) -> bool {
167    neighboring_positions(pos1).contains(&pos2)
168}
169
170/// Returns `true` if the given list of positions consists of subsequent neighbors.
171pub fn is_path_consistent(poss: &[Pos]) -> bool {
172    assert!(poss.len() >= 2);
173
174    poss.windows(2).all(|pair| {
175        if let &[p1, p2] = pair {
176            are_neighbors(p1, p2)
177        } else {
178            unreachable!();
179        }
180    })
181}
182
183pub fn are_grouped(poss: &[Pos]) -> bool {
184    let mut rt = Crepe::new();
185    rt.extend(poss.iter().copied().map(Position));
186    let (groups,) = rt.run();
187
188    poss.iter()
189        .permutations(2)
190        .all(|pair| groups.contains(&Grouped(*pair[0], *pair[1])))
191}
192
193#[cfg(test)]
194mod tests {
195    use itertools::Itertools;
196
197    use super::*;
198
199    #[test]
200    fn ring_offsets() {
201        let offsets = [0, 1, 7, 19, 37, 61, 91];
202
203        for (i, offset) in offsets.into_iter().enumerate() {
204            assert_eq!(ring_offset(i), offset);
205        }
206    }
207
208    #[test]
209    fn position_rings() {
210        let offsets = [0, 1, 7, 19, 37, 61, 91];
211
212        for (i, window) in offsets.windows(2).enumerate() {
213            if let [beg, end] = window {
214                for pos in *beg..*end {
215                    assert_eq!(ring(pos), i);
216                }
217            }
218        }
219    }
220
221    #[test]
222    fn ring_tips() {
223        for pos in 1..=6 {
224            assert!(is_at_ring_tip(pos), "{}", pos);
225        }
226
227        for pos in [7, 9, 11, 13, 15, 17] {
228            assert!(is_at_ring_tip(pos), "{}", pos);
229        }
230
231        for pos in [61, 66, 71, 76, 81, 86] {
232            assert!(is_at_ring_tip(pos), "{}", pos);
233        }
234    }
235
236    #[test]
237    fn ring_edges() {
238        for pos in [8, 10, 12, 14, 16, 18] {
239            assert!(!is_at_ring_tip(pos), "{}", pos);
240        }
241
242        for pos in (62..66)
243            .chain(67..71)
244            .chain(72..76)
245            .chain(77..81)
246            .chain(82..86)
247        {
248            assert!(!is_at_ring_tip(pos), "{}", pos);
249        }
250    }
251
252    #[test]
253    fn edge_indices_non_tips() {
254        for pos in [8, 21].into_iter().chain(38..=40).chain(62..=65) {
255            assert_eq!(ring_edge_index(pos), 0);
256        }
257        for pos in [10, 23, 24].into_iter().chain(42..=44).chain(67..=70) {
258            assert_eq!(ring_edge_index(pos), 1);
259        }
260        for pos in [12, 26, 27].into_iter().chain(46..=48).chain(72..=75) {
261            assert_eq!(ring_edge_index(pos), 2);
262        }
263        for pos in [14, 29, 30].into_iter().chain(50..=52).chain(77..=80) {
264            assert_eq!(ring_edge_index(pos), 3);
265        }
266        for pos in [16, 32, 33].into_iter().chain(54..=56).chain(82..=85) {
267            assert_eq!(ring_edge_index(pos), 4);
268        }
269        for pos in [18, 35, 36].into_iter().chain(58..=60).chain(87..=90) {
270            assert_eq!(ring_edge_index(pos), 5);
271        }
272    }
273
274    #[test]
275    fn ring_neighbors() {
276        assert_eq!(ring_neighboring_positions(1), [6, 2]);
277        assert_eq!(ring_neighboring_positions(2), [1, 3]);
278        assert_eq!(ring_neighboring_positions(3), [2, 4]);
279        assert_eq!(ring_neighboring_positions(4), [3, 5]);
280        assert_eq!(ring_neighboring_positions(5), [4, 6]);
281        assert_eq!(ring_neighboring_positions(6), [5, 1]);
282        assert_eq!(ring_neighboring_positions(18), [17, 7]);
283        assert_eq!(ring_neighboring_positions(58), [57, 59]);
284    }
285
286    #[test]
287    fn ring_tip_neighbors() {
288        assert_eq!(neighboring_positions(1), [7, 8, 2, 0, 6, 18]);
289        assert_eq!(neighboring_positions(2), [8, 9, 10, 3, 0, 1]);
290        assert_eq!(neighboring_positions(3), [2, 10, 11, 12, 4, 0]);
291        assert_eq!(neighboring_positions(4), [0, 3, 12, 13, 14, 5]);
292        assert_eq!(neighboring_positions(5), [6, 0, 4, 14, 15, 16]);
293        assert_eq!(neighboring_positions(6), [18, 1, 0, 5, 16, 17]);
294        assert_eq!(neighboring_positions(7), [19, 20, 8, 1, 18, 36]);
295        assert_eq!(neighboring_positions(9), [21, 22, 23, 10, 2, 8]);
296        assert_eq!(neighboring_positions(11), [10, 24, 25, 26, 12, 3]);
297        assert_eq!(neighboring_positions(13), [4, 12, 27, 28, 29, 14]);
298        assert_eq!(neighboring_positions(15), [16, 5, 14, 30, 31, 32]);
299        assert_eq!(neighboring_positions(17), [35, 18, 6, 16, 33, 34]);
300        assert_eq!(neighboring_positions(28), [13, 27, 48, 49, 50, 29]);
301        assert_eq!(neighboring_positions(53), [54, 31, 52, 80, 81, 82]);
302        assert_eq!(neighboring_positions(57), [87, 58, 34, 56, 85, 86]);
303    }
304
305    #[test]
306    fn ring_edge_neighbors() {
307        assert_eq!(neighboring_positions(8), [20, 21, 9, 2, 1, 7]);
308        assert_eq!(neighboring_positions(10), [9, 23, 24, 11, 3, 2]);
309        assert_eq!(neighboring_positions(12), [3, 11, 26, 27, 13, 4]);
310        assert_eq!(neighboring_positions(14), [5, 4, 13, 29, 30, 15]);
311        assert_eq!(neighboring_positions(16), [17, 6, 5, 15, 32, 33]);
312        assert_eq!(neighboring_positions(18), [36, 7, 1, 6, 17, 35]);
313        assert_eq!(neighboring_positions(38), [62, 63, 39, 20, 19, 37]);
314        assert_eq!(neighboring_positions(40), [64, 65, 41, 22, 21, 39]);
315        assert_eq!(neighboring_positions(42), [41, 67, 68, 43, 23, 22]);
316        assert_eq!(neighboring_positions(44), [43, 69, 70, 45, 25, 24]);
317        assert_eq!(neighboring_positions(46), [25, 45, 72, 73, 47, 26]);
318        assert_eq!(neighboring_positions(48), [27, 47, 74, 75, 49, 28]);
319        assert_eq!(neighboring_positions(50), [29, 28, 49, 77, 78, 51]);
320        assert_eq!(neighboring_positions(52), [31, 30, 51, 79, 80, 53]);
321        assert_eq!(neighboring_positions(54), [55, 32, 31, 53, 82, 83]);
322        assert_eq!(neighboring_positions(56), [57, 34, 33, 55, 84, 85]);
323        assert_eq!(neighboring_positions(58), [88, 59, 35, 34, 57, 87]);
324        assert_eq!(neighboring_positions(60), [90, 37, 19, 36, 59, 89]);
325    }
326
327    #[test]
328    fn groups() {
329        assert!([2, 8, 9]
330            .into_iter()
331            .permutations(3)
332            .all(|perm| are_grouped(&perm)));
333        assert!([1, 0, 4]
334            .into_iter()
335            .permutations(3)
336            .all(|perm| are_grouped(&perm)));
337        assert!([71, 45, 25, 24, 23, 22, 41, 66]
338            .into_iter()
339            .permutations(8)
340            .all(|perm| are_grouped(&perm)));
341        assert!([0, 1, 2, 3, 4, 5, 6]
342            .into_iter()
343            .permutations(7)
344            .all(|perm| are_grouped(&perm)));
345        assert!([5, 17, 18]
346            .into_iter()
347            .permutations(3)
348            .all(|perm| !are_grouped(&perm)));
349        assert!([2, 3, 5, 6]
350            .into_iter()
351            .permutations(4)
352            .all(|perm| !are_grouped(&perm)));
353        assert!([1, 4]
354            .into_iter()
355            .permutations(2)
356            .all(|perm| !are_grouped(&perm)));
357
358        assert!(are_grouped(&[11, 10, 2, 1, 6, 5, 15, 30, 29, 28, 27, 26]));
359        assert!(!are_grouped(&[
360            1, 2, 3, 4, 5, 16, 17, 35, 36, 20, 21, 22, 23, 24, 25, 26
361        ]));
362        assert!(are_grouped(&[
363            1, 2, 3, 4, 5, 16, 17, 35, 36, 19, 20, 21, 22, 23, 24, 25, 26
364        ]));
365    }
366
367    #[test]
368    fn directional_neighbor_iter() {
369        use DirectionalNeighborIter as DNI;
370
371        assert_eq!(
372            DNI::new(75, 0).take(9).collect::<Vec<_>>(),
373            vec![48, 27, 12, 3, 2, 8, 20, 38, 62]
374        );
375        assert_eq!(
376            DNI::new(76, 0).take(10).collect::<Vec<_>>(),
377            vec![49, 28, 13, 4, 0, 1, 7, 19, 37, 61]
378        );
379        assert_eq!(
380            DNI::new(77, 0).take(9).collect::<Vec<_>>(),
381            vec![50, 29, 14, 5, 6, 18, 36, 60, 90]
382        );
383
384        assert_eq!(
385            DNI::new(80, 1).take(9).collect::<Vec<_>>(),
386            vec![52, 30, 14, 4, 3, 10, 23, 42, 67]
387        );
388        assert_eq!(
389            DNI::new(81, 1).take(10).collect::<Vec<_>>(),
390            vec![53, 31, 15, 5, 0, 2, 9, 22, 41, 66]
391        );
392        assert_eq!(
393            DNI::new(82, 1).take(9).collect::<Vec<_>>(),
394            vec![54, 32, 16, 6, 1, 8, 21, 40, 65]
395        );
396
397        assert_eq!(
398            DNI::new(85, 2).take(9).collect::<Vec<_>>(),
399            vec![56, 33, 16, 5, 4, 12, 26, 46, 72]
400        );
401        assert_eq!(
402            DNI::new(86, 2).take(10).collect::<Vec<_>>(),
403            vec![57, 34, 17, 6, 0, 3, 11, 25, 45, 71]
404        );
405        assert_eq!(
406            DNI::new(87, 2).take(9).collect::<Vec<_>>(),
407            vec![58, 35, 18, 1, 2, 10, 24, 44, 70]
408        );
409    }
410}