1use crate::prelude::*;
15use alloc::collections::BTreeSet;
16
17use shuyi_assets::tileset::mix;
18use shuyi_core::TerrainId;
19
20use super::scene::MapScene;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Shown {
26 size: (i32, i32),
28 terrain: Vec<Option<TerrainId>>,
31}
32
33impl Shown {
34 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 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 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 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 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
103pub 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 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 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 let lake = scene(&[".....", ".....", "..~..", ".....", "....."]);
158 assert_eq!(
159 mixes(&lake, &[WATER]),
160 [
161 ".... .... .... ....",
162 ".... ...# ..#. ....",
163 ".... .#.. #... ....",
164 ".... .... .... ....",
165 ]
166 );
167 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 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 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 let river = scene(&["..~..", "..~..", "..=..", "..~..", "..~.."]);
199 assert_eq!(
202 mixes(&river, &[WATER, BRIDGE]),
203 [
204 ".... .#.# #.#. ....",
205 ".... .#.# #.#. ....",
206 ".... .#.# #.#. ....",
207 ".... .#.# #.#. ....",
208 ]
209 );
210 assert_eq!(
212 mixes(&river, &[BRIDGE])[1..3],
213 [".... ...# ..#. ....", ".... .#.. #... ...."]
214 );
215 let shown = Shown::of(&river);
217 let water = BTreeSet::from([WATER]);
218 assert_eq!(mix_of(shown.sides(1, 1), &water), 0b1010);
219 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 let small = scene(&[" ", " ", " .. ", " .~ ", " ", " "]);
234 let shown = Shown::of(&small);
235 assert_eq!(shown.at(0, 0), Some(GRASS));
237 assert_eq!(shown.at(-1, 2), Some(GRASS));
238 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 assert_eq!(shown.at(3, 1), Some(GRASS));
244 assert_eq!(
247 mixes(&small, &[WATER]),
248 [
249 ".... .... .... .... ....",
250 ".... .... .... .... ....",
251 ".... .... ...# ..## ..##",
252 ".... .... .#.# #### ####",
253 ".... .... .#.# #### ####",
254 ]
255 );
256 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 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 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 #[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 #[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}