Skip to main content

shuyi_ui/grid/
corners.rs

1//! The corner rule (game ADR-0052): which tiles a picture drawn *between*
2//! tiles joins.
3//!
4//! A sprite skin's layers draw one picture at every point where four tiles
5//! meet, chosen by which of those four are in the layer. Two pictures side
6//! by side share two of their tiles, so they always agree on the edge they
7//! share, and a terrain's pictures end where its tiles end.
8//!
9//! [`Shown`] is the terrain a skin joins pictures by: the view's, with the
10//! ring of tiles just outside it, so the picture at the view's edge is the
11//! one the next tile out would give. A tile off the map counts as the
12//! nearest tile on it: the ground runs on to the map's edge unchanged.
13
14use crate::prelude::*;
15use alloc::collections::BTreeSet;
16
17use shuyi_assets::tileset::mix;
18use shuyi_core::TerrainId;
19
20use super::scene::MapScene;
21
22/// The terrain shown on every tile of a scene's view and of the ring of
23/// tiles just outside it.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Shown {
26    /// The view's size in tiles, across × down.
27    size: (i32, i32),
28    /// Row by row from the tile up and left of the view's first:
29    /// `(size.0 + 2) × (size.1 + 2)`.
30    terrain: Vec<Option<TerrainId>>,
31}
32
33impl Shown {
34    /// What `scene` shows: each tile's terrain, or what a spell being
35    /// aimed would turn it into; a tile off the map (or one the scene
36    /// doesn't say) as the nearest tile that has terrain.
37    pub fn of<U>(scene: &MapScene<U>) -> Self {
38        let (w, h) = scene.size;
39        let raw = |dx: i32, dy: i32| {
40            let tile = scene.tile_at(dx, dy).filter(|t| t.terrain.is_some());
41            match tile {
42                Some(tile) => tile.becomes.or(tile.terrain),
43                None => scene.terrain_near(dx, dy),
44            }
45        };
46        let places = || (-1..=h).flat_map(|dy| (-1..=w).map(move |dx| (dx, dy)));
47        // The box round every tile that has terrain: (left, right, top,
48        // bottom).
49        let known = places().filter(|&(dx, dy)| raw(dx, dy).is_some());
50        let bounds = known.fold(None, |bounds: Option<(i32, i32, i32, i32)>, (dx, dy)| {
51            let (left, right, top, bottom) = bounds.unwrap_or((dx, dx, dy, dy));
52            Some((left.min(dx), right.max(dx), top.min(dy), bottom.max(dy)))
53        });
54        let nearest = |dx: i32, dy: i32| {
55            let (left, right, top, bottom) = bounds?;
56            raw(dx.clamp(left, right), dy.clamp(top, bottom))
57        };
58        let terrain = places()
59            .map(|(dx, dy)| raw(dx, dy).or_else(|| nearest(dx, dy)))
60            .collect();
61        Self {
62            size: scene.size,
63            terrain,
64        }
65    }
66
67    /// The terrain shown on the tile `dx` right of and `dy` below the
68    /// view's first: a tile of the view or of the ring round it. `None`
69    /// further out, or when no tile has terrain.
70    pub fn at(&self, dx: i32, dy: i32) -> Option<TerrainId> {
71        let (w, h) = self.size;
72        if !(-1..=w).contains(&dx) || !(-1..=h).contains(&dy) {
73            return None;
74        }
75        let index = usize::try_from((dy + 1) * (w + 2) + dx + 1).ok()?;
76        self.terrain.get(index).copied().flatten()
77    }
78
79    /// The four tiles that meet at the top-left corner of the tile `i`
80    /// right of and `j` below the view's first: the ones up and left of
81    /// the point, up and right, down and left, down and right.
82    pub fn corners(&self, i: i32, j: i32) -> [Option<TerrainId>; 4] {
83        [
84            self.at(i - 1, j - 1),
85            self.at(i, j - 1),
86            self.at(i - 1, j),
87            self.at(i, j),
88        ]
89    }
90
91    /// The four neighbours of the tile `dx` right of and `dy` below the
92    /// view's first: up, right, down, left.
93    pub fn sides(&self, dx: i32, dy: i32) -> [Option<TerrainId>; 4] {
94        [
95            self.at(dx, dy - 1),
96            self.at(dx + 1, dy),
97            self.at(dx, dy + 1),
98            self.at(dx - 1, dy),
99        ]
100    }
101}
102
103/// The [`mix`] of `tiles` being one of `of`.
104pub fn mix_of(tiles: [Option<TerrainId>; 4], of: &BTreeSet<TerrainId>) -> u8 {
105    mix(tiles.map(|tile| tile.is_some_and(|id| of.contains(&id))))
106}
107
108#[cfg(test)]
109mod tests {
110    use proptest::prelude::*;
111    use shuyi_assets::tileset::mix_name;
112    use shuyi_core::Pos;
113
114    use super::*;
115    use crate::grid::testing::any_scene;
116    use crate::toy::grid::{BRIDGE, GRASS, Piece, WATER};
117
118    /// A scene showing `rows` (`.` grass, `~` water, `=` bridge, a space
119    /// off the map), its ring included: the view is the rows without
120    /// their first and last, and each without its first and last tile.
121    fn scene(rows: &[&str]) -> MapScene<Piece> {
122        let at = |pos: Pos| {
123            let row = rows.get(usize::try_from(pos.y + 1).ok()?)?;
124            match row.chars().nth(usize::try_from(pos.x + 1).ok()?)? {
125                '.' => Some(GRASS),
126                '~' => Some(WATER),
127                '=' => Some(BRIDGE),
128                _ => None,
129            }
130        };
131        let across = rows.first().map_or(0, |r| r.chars().count());
132        let size = |n: usize| i32::try_from(n).unwrap() - 2;
133        let mut scene = MapScene::new(Pos::new(0, 0), (size(across), size(rows.len())));
134        scene.set_terrain(at);
135        scene
136    }
137
138    /// The mixes of `of` at every corner point of `scene`'s view, row by
139    /// row, each as its four places written out.
140    fn mixes(scene: &MapScene<Piece>, of: &[TerrainId]) -> Vec<String> {
141        let shown = Shown::of(scene);
142        let of: BTreeSet<TerrainId> = of.iter().copied().collect();
143        let (w, h) = scene.size;
144        (0..=h)
145            .map(|j| {
146                let row: Vec<String> = (0..=w)
147                    .map(|i| mix_name(mix_of(shown.corners(i, j), &of)))
148                    .collect();
149                row.join(" ")
150            })
151            .collect()
152    }
153
154    #[test]
155    fn a_lake_is_ringed_by_the_mixes_of_its_corners() {
156        // One water tile in the middle of a 3 × 3 view of grass.
157        let lake = scene(&[".....", ".....", "..~..", ".....", "....."]);
158        assert_eq!(
159            mixes(&lake, &[WATER]),
160            [
161                ".... .... .... ....",
162                ".... ...# ..#. ....",
163                ".... .#.. #... ....",
164                ".... .... .... ....",
165            ]
166        );
167        // The grass is everywhere else.
168        assert_eq!(
169            mixes(&lake, &[GRASS])[1..3],
170            ["#### ###. ##.# ####", "#### #.## .### ####"]
171        );
172        let shown = Shown::of(&lake);
173        assert_eq!(shown.at(1, 1), Some(WATER));
174        assert_eq!(shown.at(0, 1), Some(GRASS));
175        // The ring is there; one tile further isn't.
176        assert_eq!(shown.at(-1, -1), Some(GRASS));
177        assert_eq!(shown.at(3, 3), Some(GRASS));
178        for (dx, dy) in [(-2, 0), (4, 0), (0, -2), (0, 4)] {
179            assert_eq!(shown.at(dx, dy), None, "({dx}, {dy})");
180        }
181        // Up, right, down, left of the lake and of the tile left of it.
182        assert_eq!(shown.sides(1, 1), [Some(GRASS); 4]);
183        assert_eq!(
184            shown.sides(0, 1),
185            [Some(GRASS), Some(WATER), Some(GRASS), Some(GRASS)]
186        );
187        let water = BTreeSet::from([WATER]);
188        assert_eq!(mix_of(shown.sides(0, 1), &water), 0b0100);
189        assert_eq!(mix_of(shown.sides(1, 0), &water), 0b0010);
190        assert_eq!(mix_of(shown.sides(2, 1), &water), 0b0001);
191        assert_eq!(mix_of(shown.sides(1, 2), &water), 0b1000);
192    }
193
194    #[test]
195    fn a_river_runs_on_past_the_view_and_a_bridge_is_what_its_layer_says() {
196        // A river down the middle column, running on above and below the
197        // view, with a bridge on its middle tile.
198        let river = scene(&["..~..", "..~..", "..=..", "..~..", "..~.."]);
199        // The bridge counts as water for the shore: it is unbroken, and
200        // runs to the view's edge.
201        assert_eq!(
202            mixes(&river, &[WATER, BRIDGE]),
203            [
204                ".... .#.# #.#. ....",
205                ".... .#.# #.#. ....",
206                ".... .#.# #.#. ....",
207                ".... .#.# #.#. ....",
208            ]
209        );
210        // As its own layer it is one tile.
211        assert_eq!(
212            mixes(&river, &[BRIDGE])[1..3],
213            [".... ...# ..#. ....", ".... .#.. #... ...."]
214        );
215        // The bridge has water above and below: it crosses left to right.
216        let shown = Shown::of(&river);
217        let water = BTreeSet::from([WATER]);
218        assert_eq!(mix_of(shown.sides(1, 1), &water), 0b1010);
219        // A river the view cuts off, with nothing given outside it, runs
220        // on the same.
221        let mut cut = river.clone();
222        cut.rim = vec![None; cut.rim.len()];
223        assert_eq!(
224            mixes(&cut, &[WATER, BRIDGE]),
225            mixes(&river, &[WATER, BRIDGE])
226        );
227    }
228
229    #[test]
230    fn a_tile_off_the_map_counts_as_the_nearest_tile_on_it() {
231        // A 2 × 2 map in the middle of a 4 × 4 view: grass, but water at
232        // its bottom-right.
233        let small = scene(&["      ", "      ", "  ..  ", "  .~  ", "      ", "      "]);
234        let shown = Shown::of(&small);
235        // Above and left of the map: its top-left tile's.
236        assert_eq!(shown.at(0, 0), Some(GRASS));
237        assert_eq!(shown.at(-1, 2), Some(GRASS));
238        // Right of and below the water: water.
239        assert_eq!(shown.at(3, 2), Some(WATER));
240        assert_eq!(shown.at(2, 4), Some(WATER));
241        assert_eq!(shown.at(4, 4), Some(WATER));
242        // Right of the grass above it: grass.
243        assert_eq!(shown.at(3, 1), Some(GRASS));
244        // So the water's shore is only inside the map: at the point in
245        // its middle. Along the map's edge it runs straight out.
246        assert_eq!(
247            mixes(&small, &[WATER]),
248            [
249                ".... .... .... .... ....",
250                ".... .... .... .... ....",
251                ".... .... ...# ..## ..##",
252                ".... .... .#.# #### ####",
253                ".... .... .#.# #### ####",
254            ]
255        );
256        // No terrain anywhere: nothing shown, nothing in any layer.
257        let empty = MapScene::<Piece>::new(Pos::new(0, 0), (2, 2));
258        let shown = Shown::of(&empty);
259        assert_eq!(shown.at(0, 0), None);
260        assert_eq!(mix_of(shown.corners(1, 1), &BTreeSet::from([GRASS])), 0);
261        // A view of no tiles still has its ring.
262        let none = Shown::of(&MapScene::<Piece>::new(Pos::new(0, 0), (0, 0)));
263        assert_eq!(none.terrain.len(), 4);
264        assert_eq!(none.at(0, 0), None);
265    }
266
267    #[test]
268    fn a_tile_a_spell_would_change_is_shown_as_what_it_would_become() {
269        let mut lake = scene(&[".....", ".....", "..~..", ".....", "....."]);
270        lake.tiles[4].becomes = Some(GRASS);
271        lake.tiles[0].becomes = Some(WATER);
272        let shown = Shown::of(&lake);
273        assert_eq!(shown.at(1, 1), Some(GRASS));
274        assert_eq!(shown.at(0, 0), Some(WATER));
275        assert_eq!(mixes(&lake, &[WATER])[0], "...# ..#. .... ....");
276        // A tile off the map becomes nothing.
277        let mut off = scene(&["   ", " . ", "   "]);
278        off.tiles[0].terrain = None;
279        off.tiles[0].becomes = Some(WATER);
280        assert_eq!(Shown::of(&off).at(0, 0), None);
281    }
282
283    #[test]
284    fn a_mix_counts_only_the_tiles_of_the_layer() {
285        let of = BTreeSet::from([WATER, BRIDGE]);
286        let tiles = [Some(WATER), Some(GRASS), None, Some(BRIDGE)];
287        assert_eq!(mix_of(tiles, &of), 0b1001);
288        assert_eq!(mix_of([None; 4], &of), 0);
289        assert_eq!(mix_of([Some(WATER); 4], &of), 15);
290        assert_eq!(mix_of([Some(WATER); 4], &BTreeSet::new()), 0);
291    }
292
293    proptest! {
294        /// Whatever the scene, two pictures side by side (or one above
295        /// the other) join the same two tiles along the edge they share,
296        /// and the picture round a tile's corners all name that tile.
297        #[test]
298        fn neighbouring_corner_points_share_their_tiles(scene in any_scene()) {
299            let shown = Shown::of(&scene);
300            let (w, h) = scene.size;
301            for j in 0..=h {
302                for i in 0..=w {
303                    let [top_left, top_right, bottom_left, bottom_right] = shown.corners(i, j);
304                    let right = shown.corners(i + 1, j);
305                    let below = shown.corners(i, j + 1);
306                    if i < w {
307                        prop_assert_eq!((top_right, bottom_right), (right[0], right[2]));
308                    }
309                    if j < h {
310                        prop_assert_eq!((bottom_left, bottom_right), (below[0], below[1]));
311                    }
312                    prop_assert_eq!(bottom_right, shown.at(i, j));
313                    prop_assert_eq!(top_left, shown.at(i - 1, j - 1));
314                }
315            }
316        }
317
318        /// Whatever the scene, a tile of the view that is on the map
319        /// shows its own terrain (or what it would become), and the rest
320        /// show terrain only if some tile has any.
321        #[test]
322        fn a_tile_on_the_map_shows_its_own_terrain(scene in any_scene()) {
323            let shown = Shown::of(&scene);
324            let (w, h) = scene.size;
325            let mut any = false;
326            for dy in -1..=h {
327                for dx in -1..=w {
328                    let own = scene.tile_at(dx, dy).filter(|t| t.terrain.is_some());
329                    if let Some(tile) = own {
330                        prop_assert_eq!(shown.at(dx, dy), tile.becomes.or(tile.terrain));
331                    }
332                    any |= scene.terrain_near(dx, dy).is_some();
333                }
334            }
335            if !any {
336                prop_assert!(shown.terrain.iter().all(Option::is_none));
337            }
338        }
339    }
340}