hext_boards/board/
mod.rs

1use std::{collections::HashMap, fmt::Display};
2
3use glam::{ivec2, IVec2};
4
5mod test;
6
7const LEFT_BRACKET: char = '⟨';
8const RIGHT_BRACKET: char = '⟩';
9
10/// Convert from hexagonal coordinates to cartesian coordinates
11fn hexagonal_to_cartesian(hexagonal_coords: IVec2) -> IVec2 {
12    let b1 = ivec2(-5, -1);
13    let b2 = ivec2(5, -1);
14
15    b1 * hexagonal_coords.x + b2 * hexagonal_coords.y
16}
17
18const STATIC_TILE_ELEMENTS: [(IVec2, char); 8] = [
19    (ivec2(-1, 1), '-'),
20    (ivec2(0, 1), '-'),
21    (ivec2(1, 1), '-'),
22    (ivec2(-3, 0), LEFT_BRACKET),
23    (ivec2(3, 0), RIGHT_BRACKET),
24    (ivec2(-1, -1), '-'),
25    (ivec2(0, -1), '-'),
26    (ivec2(1, -1), '-'),
27];
28
29const DYNAMIC_TILE_ELEMENTS: [(IVec2, char, char); 4] = [
30    (ivec2(-2, 1), '\\', RIGHT_BRACKET),
31    (ivec2(2, 1), '/', LEFT_BRACKET),
32    (ivec2(-2, -1), '/', RIGHT_BRACKET),
33    (ivec2(2, -1), '\\', LEFT_BRACKET),
34];
35
36/// A board composed of hexagons.
37///
38/// You can construct a [`HexagonalBoard`] the same ways you would construct a [`HashMap`],
39/// where the keys are something that can be converted to a [`glam::IVec2`] (e.g., `[i32; 2]`).
40///
41/// The positions are interpreted as hexagonal coordinates, where the basis `[1, 0]` is the
42/// hexagon to the left and up, and `[0, 1]` is the one to the right and up.
43///
44/// You can render a [`HexagonalBoard`] using [`HexagonalBoard::render`] if `T: Into<char> +
45/// Clone`. Otherwise, you can use [`HexagonalBoard::render_with`] to specify how to convert the
46/// `T` into a [`char`].
47///
48/// You can also use [`HexagonalBoard::char_map`] if you want to easily get what character should
49/// be printed where, but you want to do the rendering yourself.
50///
51/// # Examples
52///
53/// Basic usage:
54///
55/// ```rust
56/// use hext_boards::HexagonalBoard;
57///
58/// // Put an `'A'` at `[0, 0]` and a `'B'` at `[1, 1]`.
59/// let board = HexagonalBoard::from([
60///     ([0, 0], 'A'),
61///     ([1, 1], 'B'),
62/// ]);
63///
64/// let output = board.render();
65/// let expected = indoc::indoc!(
66///     r"
67///          /---\
68///         ⟨  B  ⟩
69///          ⟩---⟨
70///         ⟨  A  ⟩
71///          \---/
72///     "
73/// ).trim_end_matches('\n');
74///
75/// println!("{output}");
76///
77/// assert_eq!(output, expected)
78/// ```
79///
80/// Using [`HexagonalBoard::render_with`]:
81///
82/// ```rust
83/// use hext_boards::HexagonalBoard;
84///
85/// let board = HexagonalBoard::from([
86///     ([0, 0], 5),
87///     ([0, 1], 13),
88///     ([1, 1], 25),
89/// ]);
90///
91/// // Everything needs to be one char, so we have to use hexadecimal or some other radix to
92/// // output higher numbers.
93/// let output = board.render_with(|n| char::from_digit(*n, 36).expect("`n` is less than 36."));
94///
95/// let expected = indoc::indoc!(
96///     r"
97///          /---\
98///         ⟨  p  ⟩---\
99///          ⟩---⟨  d  ⟩
100///         ⟨  5  ⟩---/
101///          \---/
102///     "
103/// ).trim_end_matches('\n');
104///
105/// assert_eq!(output, expected)
106/// ```
107#[derive(Debug, Clone, Default)]
108pub struct HexagonalBoard<T> {
109    values: HashMap<IVec2, T>,
110}
111
112// TODO: I shouldn't have to depend on `Copy` but whatever
113impl<T> HexagonalBoard<T> {
114    /// Map from coordinates
115    pub fn char_map(&self, into_char: impl Fn(&T) -> char) -> HashMap<IVec2, char> {
116        let min_x = self
117            .values
118            .keys()
119            .map(|pos| pos.x - pos.y)
120            .max()
121            .unwrap_or(0);
122        let max_y = self
123            .values
124            .keys()
125            .map(|pos| pos.x + pos.y)
126            .max()
127            .unwrap_or(0);
128
129        let origin_offset = ivec2(5 * min_x + 3, max_y + 1);
130
131        let mut output = HashMap::new();
132
133        for (hexagonal_coords, value) in &self.values {
134            // Get cartesian coordinates
135            let cartesian_coords = hexagonal_to_cartesian(*hexagonal_coords) + origin_offset;
136
137            // Insert value in center
138            output.insert(cartesian_coords, into_char(value));
139
140            // Add top and sides, which are always the same
141            for (offset, char) in STATIC_TILE_ELEMENTS {
142                output.insert(cartesian_coords + offset, char);
143            }
144
145            // Add edges, which depend if a tile has neighbours
146            for (offset, single, multiple) in DYNAMIC_TILE_ELEMENTS {
147                match output.get(&(cartesian_coords + offset)) {
148                    None => output.insert(cartesian_coords + offset, single),
149                    Some(&RIGHT_BRACKET | &LEFT_BRACKET) => None,
150                    Some(_) => output.insert(cartesian_coords + offset, multiple),
151                };
152            }
153        }
154
155        output
156    }
157
158    /// Renders the hexagonal board into a [`String`], converting the `T`s into [`char`]s using
159    /// `into_char`.
160    ///
161    /// If `T` can easily be converted to a [`char`] (i.e., `T: Into<char> + Copy`), you can
162    /// use [`Self::render`].
163    pub fn render_with(&self, into_char: impl Fn(&T) -> char) -> String {
164        render_char_map(self.char_map(into_char).into_iter().collect())
165    }
166
167    /// Renders the hexagonal board into a [`String`].
168    ///
169    /// To specify how to convert the `T` into a [`char`], see [`Self::render_with`].
170    pub fn render(&self) -> String
171    where
172        for<'a> char: From<T>,
173        T: Copy,
174    {
175        self.render_with(|t| char::from(*t))
176    }
177}
178
179fn render_char_map(mut char_map: Vec<(IVec2, char)>) -> String {
180    char_map.sort_unstable_by_key(|(pos, _)| (pos.y, pos.x));
181    let mut output = String::with_capacity(char_map[char_map.len() - 1].0.x as usize);
182
183    let mut cursor = IVec2::new(0, 0);
184    for (position, char) in char_map {
185        while cursor.y < position.y {
186            output.push('\n');
187            cursor.y += 1;
188            cursor.x = 0;
189        }
190
191        while cursor.x < position.x {
192            output.push(' ');
193            cursor.x += 1;
194        }
195
196        output.push(char);
197        cursor.x += 1;
198    }
199
200    output
201}
202
203impl<P: Into<IVec2>, T, V> From<V> for HexagonalBoard<T>
204where
205    V: IntoIterator<Item = (P, T)>,
206{
207    fn from(value: V) -> Self {
208        value.into_iter().collect()
209    }
210}
211
212impl<P: Into<IVec2>, T> FromIterator<(P, T)> for HexagonalBoard<T> {
213    fn from_iter<I: IntoIterator<Item = (P, T)>>(iter: I) -> Self {
214        Self {
215            values: iter
216                .into_iter()
217                .map(|(position, char)| (position.into(), char))
218                .collect(),
219        }
220    }
221}
222
223impl<T> Display for HexagonalBoard<T>
224where
225    for<'a> char: From<T>,
226    T: Copy,
227{
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        write!(f, "{}", self.render())
230    }
231}