use std::fmt::{write, Debug, Formatter}; use crate::utils::IntoTile; /// The size of the grid that can be matched; equal to the length of one side of the square grid const RULE_MAGNITUDE: usize = 7; /// Number of array entries required to store all the data for a grid const TILE_GRID_SIZE: usize = RULE_MAGNITUDE.pow(2); /// The index of the center tile in the grid const GRID_CENTER: usize = (TILE_GRID_SIZE - 1) / 2; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct NotSquareError; impl std::fmt::Display for NotSquareError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Input is not a square grid") } } impl std::error::Error for NotSquareError {} /// Represents how a single tile location should be matched when evaluating a rule #[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Default, Copy, Clone)] pub enum TileStatus { /// This tile will always match, regardless of the input tile #[default] Ignore, /// This tile will only match when there is no input tile (`None`) Nothing, /// This tile will always match as long as the tile exists (`Option::is_some`) Anything, /// This tile will match as long as the input tile exists and the input value is the same as this value Is(i32), /// This tile will match as long as the input tile exists and the input value is anything other than this value IsNot(i32), } impl Debug for TileStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Ignore => write!(f, "Any Or No Tile"), Self::Nothing => write!(f, "No Tile"), Self::Anything => write!(f, "Any Tile"), Self::Is(value) => write!(f, "Must Match [{}]", value), Self::IsNot(value) => write!(f, "Must Not Match [{}]", value), } } } impl PartialEq<Option<i32>> for TileStatus { fn eq(&self, other: &Option<i32>) -> bool { let matched = match self { Self::Ignore => true, Self::Nothing => other.is_none(), Self::Anything => other.is_some(), Self::Is(value) => &Some(*value) == other, Self::IsNot(value) => &Some(*value) != other, }; matched } } impl TileStatus { #[deprecated(since = "0.2.0", note = "Use `TileStatus::into` directly instead")] pub fn to_ldtk_value(self) -> i64 { self.into_tile() as i64 } #[deprecated(since = "0.2.0", note = "Use `TileStatus::from` directly instead")] pub fn from_ldtk_value(value: i64) -> Self { Self::from(value) } } /// Holds a grid of raw input data, as a more ideal format for interop and storage #[repr(transparent)] pub struct TileLayout(pub [Option<i32>; TILE_GRID_SIZE]); impl TileLayout { /// Create a 1x1 grid of tile data pub fn single(value: i32) -> Self { let mut grid = [None; TILE_GRID_SIZE]; grid[GRID_CENTER] = Some(value); TileLayout(grid) } /// Construct a filled grid of tile data pub fn filled(values: [i32; TILE_GRID_SIZE]) -> Self { TileLayout(values.map(Some)) } /// Construct a filled grid of identical tile data pub fn spread(value: i32) -> Self { TileLayout([Some(value); TILE_GRID_SIZE]) } /// Filter the layout data so that it only contains the tiles surrounding the target tile. This /// means that the array index of every entry before the center point will match the original data /// array, but ever entry after the center point will have its index shifted down by 1 /// /// The main utility of this is to perform set operations on every tile _other_ than the target tile. /// /// ## Examples /// /// ``` /// # use micro_autotile::TileLayout; /// let layout = TileLayout::single(123); /// let has_any_surrounding_tiles = layout.surrounding() /// .iter() /// .any(|tile| tile.is_some()); /// /// assert_eq!(has_any_surrounding_tiles, false); /// ``` pub fn surrounding(&self) -> [Option<i32>; 8] { [ self.0[0], self.0[1], self.0[2], self.0[3], self.0[5], self.0[6], self.0[7], self.0[8], ] } } impl Debug for TileLayout { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if f.alternate() { let lines = as_lines(&self.0); let mut max_width = 1; for line in lines.iter() { for value in line.iter() { if let Some(value) = value { max_width = max_width.max(value.to_string().len()); } } } for line in lines { for value in line { if let Some(value) = value { write!(f, "{:^max_width$?} ", value)?; } else { write!(f, "{:^max_width$} ", "#")?; } } write!(f, "\n")?; } write!(f, "\n") } else { writeln!(f, "{:?}", self.0) } } } impl <T> TryFrom<&[T]> for TileLayout where T: IntoTile + Copy + Default + Debug { type Error = NotSquareError; fn try_from(value: &[T]) -> Result<Self, Self::Error> { if is_square(value.len()) { let formatted = transpose(value); Ok(Self(formatted.map(|t| if t.into_tile() == 0 { None } else { Some(t.into_tile()) }))) } else { Err(NotSquareError) } } } /// Holds parsed tile rules that are used to evaluate a layout #[derive(Clone, Copy)] #[repr(transparent)] pub struct TileMatcher(pub [TileStatus; TILE_GRID_SIZE]); impl Debug for TileMatcher { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if f.alternate() { let lines = as_lines(&self.0); writeln!(f, "Tile Matcher")?; for line in lines { for value in line { write!(f, "{:?} ", value)?; } write!(f, "\n")?; } writeln!(f, "\n") } else { write!(f, "TileMatcher({:?})", self.0) } } } impl Default for TileMatcher { fn default() -> Self { TileMatcher([TileStatus::default(); TILE_GRID_SIZE]) } } impl TileMatcher { pub const fn single(value: TileStatus) -> Self { let mut rules = [TileStatus::Ignore; TILE_GRID_SIZE]; rules[GRID_CENTER] = value; TileMatcher(rules) } /// Create a 1x1 matcher, with any rule for the target tile pub const fn single_match(value: i32) -> Self { Self::single(TileStatus::Is(value)) } /// Check if the given input layout of tile data conforms to this matcher pub fn matches(&self, layout: &TileLayout) -> bool { self.0 .iter() .zip(layout.0.iter()) .all(|(status, reality)| *status == *reality) } /// Load data from an LDTK JSON file. Supports arbitrary sized matchers for any square grid. /// Other sizes of matcher will result in `None` pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> { if is_square(value.len()) { Some(Self(transpose(value.as_slice()).map(TileStatus::from))) } else { None } } } impl <T> TryFrom<&[T]> for TileMatcher where T: IntoTile + Copy + Default { type Error = NotSquareError; fn try_from(value: &[T]) -> Result<Self, Self::Error> { if is_square(value.len()) { let formatted = transpose(value); Ok(Self(formatted.map(TileStatus::from))) } else { Err(NotSquareError) } } } impl TryFrom<&[TileStatus]> for TileMatcher { type Error = NotSquareError; fn try_from(value: &[TileStatus]) -> Result<Self, Self::Error> { if is_square(value.len()) { Ok(Self(transpose(value))) } else { Err(NotSquareError) } } } /// Convert a square grid of arbitrary size into one of the expected autotiler grid size (7x7). Odd /// numbered grids will be centered in the output, while even numbered grids will be offset by 1 to /// the top and left of the output. /// /// This method will panic if the provided list of values is not a square grid /// /// ## Example /// /// For a 2x2 input grid: /// /// ```text /// 2 2 /// 2 2 /// ``` /// /// Applying this function will result in the following output grid: /// /// ```text /// 1 1 1 1 1 1 1 /// 1 1 1 1 1 1 1 /// 1 1 2 2 1 1 1 /// 1 1 2 2 1 1 1 /// 1 1 1 1 1 1 1 /// 1 1 1 1 1 1 1 /// 1 1 1 1 1 1 1 /// ``` /// /// ## Example /// /// For a 3x3 input grid: /// /// ```text /// 3 3 3 /// 3 3 3 /// 3 3 3 /// ``` /// /// Applying this function will result in the following output grid: /// /// ```text /// 1 1 1 1 1 1 1 /// 1 1 1 1 1 1 1 /// 1 1 3 3 3 1 1 /// 1 1 3 3 3 1 1 /// 1 1 3 3 3 1 1 /// 1 1 1 1 1 1 1 /// 1 1 1 1 1 1 1 /// ``` /// fn transpose<Value>(input: &[Value]) -> [Value; TILE_GRID_SIZE] where Value: Default + Copy { if input.len() == TILE_GRID_SIZE { match input.try_into() { Ok(output) => return output, Err(_) => { // This should never happen since we've already checked the length - just in case, // we want it to fall through and run the rest of the algorithm } } } if !is_square(input.len()) { // Length isn't square == it does not represent a square grid panic!("Input must be a square grid"); } let input_size = (input.len() as f64).sqrt() as usize; let mut output = [Value::default(); TILE_GRID_SIZE]; let output_start = if input_size < RULE_MAGNITUDE { // Even padding requires our initial x coord to be half the size difference from the edge. // Even sized squares will be offset to by one to the left and top, which is preferable to // rejecting even squares altogether (RULE_MAGNITUDE - input_size) / 2 } else { 0 }; let input_start = if input_size > RULE_MAGNITUDE { (input_size - RULE_MAGNITUDE) / 2 } else { 0 }; for x in 0..input_size { for y in 0..input_size { let adjusted_input_x = x + input_start; let adjusted_input_y = y + input_start; let input_idx = adjusted_input_y * input_size + adjusted_input_x; let adjusted_output_x = x + output_start; let adjusted_output_y = y + output_start; let output_idx = adjusted_output_y * RULE_MAGNITUDE + adjusted_output_x; output[output_idx] = input[input_idx]; } } output } fn is_square(n: usize) -> bool { let sqrt_n = (n as f64).sqrt() as usize; n == sqrt_n.pow(2) } fn as_lines<T>(input: &[T]) -> Vec<&[T]> { input.chunks(RULE_MAGNITUDE).collect() } #[cfg(test)] mod tests { use super::*; #[test] fn it_transposes_odd_grids() { let input = [2, 2, 2, 2, 2, 2, 2, 2, 2]; let expected = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; assert_eq!(expected, transpose(&input)); } }