//! `micro_autotile` provides an implementation of the LDTK auto-tiling algorithm, for use in //! programs at runtime. The representation is compatible with that saved by LDTK, meaning that //! definitions can be loaded directly from LDTK JSON exports. //! //! Creating a single rule works like this: //! //! 1. Create a `TileMatcher` out of `TileStatus` entries. //! * Tile Matchers are squares represented as fixed size flat arrays //! * Currently only 1x1 and 3x3 matchers are supported, 5x5 matchers are incompatible //! * Since a Tile Matcher is a rule, they are usually created statically or loaded as //! an asset that will not change much / at all //! 2. Create a `TileOutput` that represents the value produces by this rule when it matches //! * An output value of `Skip` will cause the rule to be a noop. This has utility when combined //! with a rule's `chance` value, as part of a set of rules //! * A `Single` output will always produce the same value //! * A `Random` output will produce one of the provided values at random //! 3. Combine these into an `AutoTileRule` //! * There are a number of convenience methods for doing this process without mistakes in a single function call //! //! To utilise your matcher, you'll need to provide a specifically formatted slice of your input data (typically a sub-grid //! of a tile map). If you're matching a single tile, you can use the convenience method `TileLayout::single`, otherwise //! you will need to provide a 9 element array that represents 3 rows and 3 columns of data, in the following format: //! //! ```text //! Flat array data //! [1, 2, 3, 4, 5, 6, 7, 8, 9] //! ``` //! //! ```text //! Formatted in the way it would appear if laid out in a tile map //! [ //! 1, 2, 3, # First row, all three columns //! 4, 5, 6, # Second row, all three columns //! 7, 8, 9, # Third row, all three columns //! ] //! ``` //! //! As we can see, the fifth element of the array is the centre tile of our matching grid. In fact, `TileLayout::single` constructs //! a 9 element array where the fifth element is `Some(your_value)`, and the rest are simply `None`. This is possible because the //! actual data represents each element as an `Option` (not as the simple numbers above), which allows matching up against edges of //! data arrays, or against non-regular shapes. Putting this together to match against data from our tile map, we have the following: //! //! e.g. //! ```rust //! # use micro_autotile::{AutoTileRule, TileLayout, TileOutput}; //! # fn main() { //! use micro_autotile::TileMatcher; //! // Tile maps often use unsigned integers to represent different types of tiles //! const WALL_TILE: usize = 0; //! const GROUND_TILE: usize = 1; //! //! // Match a 1x1 ground tile, output the index within the spritesheet that we'll use for rendering //! let match_1_x_1 = AutoTileRule::exact(GROUND_TILE, 57); //! //! assert_eq!( //! match_1_x_1.resolve_match(&TileLayout::single(GROUND_TILE)), //! Some(57) //! ); //! //! // More realistically, we might have some tile data for a ground tile with other data arround it. //! // When we match against a rule, we're always trying to produce a value for the _central_ value in //! // our `TileLayout` (the fifth element) //! let enclosed_ground = TileLayout([ //! Some(WALL_TILE), Some(WALL_TILE), Some(WALL_TILE), //! Some(WALL_TILE), Some(GROUND_TILE), Some(WALL_TILE), //! Some(WALL_TILE), Some(WALL_TILE), Some(WALL_TILE), //! ]); //! //! assert_eq!( //! match_1_x_1.resolve_match(&enclosed_ground), //! Some(57) //! ); //! //! // There may also be situations in which you just want to know that a given layout matches a rule, without //! // concern for producing a value for that layout. You can directly use a `TileMatcher` for this //! assert!(TileMatcher::single(GROUND_TILE).matches(&enclosed_ground)); //! # } //! ``` //! //! There's already a lot of utility to these structures, but we still need to manually run a set of //! rules against our maps and do some work with the `AutoTileRule::chance` property to figure out //! what the final output should be for a given layout. //! //! Introducing the `AutoRuleSet` struct, that represents a sequence of rules that should be evaluated //! to produce an output. It provides similar methods to the individual AutoTileRule, but will execute //! against a set of rules at once. There are also convenience methods for combining AutoRuleSet //! instances //! //! ```rust //! # use micro_autotile::{AutoTileRule, AutoRuleSet, TileLayout, TileOutput}; //! # fn main() { //! use micro_autotile::{TileMatcher, TileStatus}; //! const WALL_TILE: usize = 0; //! const GROUND_TILE: usize = 1; //! const OTHER_TILE: usize = 342; //! //! let wall_rules = AutoRuleSet(vec![ //! AutoTileRule::single_when(TileMatcher([ // Top Left Corner //! TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), //! TileStatus::IsNot(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), //! TileStatus::IsNot(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), //! ]), 54), //! AutoTileRule::single_when(TileMatcher([ // Top Right Corner //! TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), TileStatus::IsNot(WALL_TILE), //! TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::IsNot(WALL_TILE), //! TileStatus::Is(WALL_TILE), TileStatus::Is(WALL_TILE), TileStatus::IsNot(WALL_TILE), //! ]), 55), //! // ... Etc //! ]); //! //! let ground_rules = AutoRuleSet(vec![ //! // Use decorated tiles in 10% of cases //! AutoTileRule::single_any_chance(GROUND_TILE, vec![45, 46, 47], 0.1), //! // Fall back to the basic tile if we don't match previously //! AutoTileRule::exact(GROUND_TILE, 44), //! ]); //! //! // Easily merge rule sets in an ordered way //! let combined_rules = wall_rules + ground_rules; //! //! let sublayout = TileLayout([ //! Some(OTHER_TILE), Some(GROUND_TILE), Some(GROUND_TILE), //! Some(WALL_TILE), Some(WALL_TILE), Some(OTHER_TILE), //! Some(WALL_TILE), Some(WALL_TILE), Some(GROUND_TILE), //! ]); //! //! // We've got a layout that represents the top right corner of a wall, the second rule in our //! // set - the value of the tiles that match "IsNot(WALL_TILE)" are irrelevant, as long as they //! // exist (Option::Some) //! let output = combined_rules.resolve_match(&sublayout); //! assert_eq!(output, Some(55)); //! # } //! ``` use std::ops::Add; /// Represents how a single tile location should be matched when evaluating a rule #[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, 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(usize), /// This tile will match as long as the input tile exists and the input value is anything other than this value IsNot(usize), } impl PartialEq<Option<usize>> for TileStatus { fn eq(&self, other: &Option<usize>) -> bool { 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, } } } impl TileStatus { pub fn to_ldtk_value(&self) -> i64 { match self { Self::Ignore => 0, Self::Nothing => -1000001, Self::Anything => 1000001, Self::Is(value) => *value as i64, Self::IsNot(value) => -(*value as i64), } } pub fn from_ldtk_value(value: i64) -> Self { match value { 0 => Self::Ignore, 1000001 => Self::Anything, -1000001 => Self::Nothing, other => { if other > 0 { Self::Is(other as usize) } else { Self::IsNot(other.unsigned_abs() as usize) } } } } } /// Holds the evaluation rules for a 3x3 grid of tiles. A 1x1 grid of tile matchers /// can be created by providing an array of `TileStatus` structs that are all `TileStatus::Ignore`, /// except for the value in the fifth position /// /// e.g. /// /// ``` /// # use micro_autotile::{TileMatcher, TileStatus}; /// let matcher = TileMatcher([ /// TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, /// TileStatus::Ignore, TileStatus::Anything, TileStatus::Ignore, /// TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, /// ]); /// ``` #[derive(Clone, Debug, Default)] #[repr(transparent)] pub struct TileMatcher(pub [TileStatus; 9]); impl TileMatcher { /// Create a 1x1 matcher, where the target tile must be the supplied `value` pub const fn single(value: usize) -> Self { Self([ TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Is(value), TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, ]) } /// Create a 1x1 matcher, with any rule for the target tile pub const fn single_match(value: TileStatus) -> Self { Self([ TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, value, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, TileStatus::Ignore, ]) } /// 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. Currently supports 1x1 and 3x3 matchers. /// Other sizes of matcher will result in `None` pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> { if value.len() == 1 { let tile = value[0]; Some(Self::single_match(TileStatus::from_ldtk_value(tile))) } else if value.len() == 9 { Some(TileMatcher( [ value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], ] .map(TileStatus::from_ldtk_value), )) } else { None } } } /// Represents a grid of input data. What this data means is dependant on your application, and /// could realistically correlate to anything. It is assumed to be a 3x3 slice of tile data from a /// tile map #[derive(Clone, Debug, Default)] #[repr(transparent)] pub struct TileLayout(pub [Option<usize>; 9]); impl TileLayout { /// Create a 1x1 grid of tile data pub fn single(value: usize) -> Self { TileLayout([None, None, None, None, Some(value), None, None, None, None]) } /// Construct a filled 3x3 grid of tile data pub fn filled(values: [usize; 9]) -> Self { TileLayout(values.map(Some)) } /// Construct a filled 3x3 grid of identical tile data pub fn spread(value: usize) -> Self { TileLayout([Some(value); 9]) } /// Filter the layout data so that it only contains the tiles surrounding the target tile. The main /// utility of this is to perform set operations on every tile _other_ than the target tile. /// /// e.g. /// /// ``` /// # 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<usize>; 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], ] } } /// Represents the value produced when a rule is matched. Will need to be inspected to find out /// the raw data value. This value will typically correspond to an index in a spritesheet, but /// there is no proscribed meaning - it will be application dependant and could represent some /// other index or meaning #[derive(Clone, Debug, Default)] pub enum TileOutput { /// This output should be skipped. Noop equivalent #[default] Skip, /// This exact value should be produces Single(usize), /// Some method should be used to select one of the values in this list Random(Vec<usize>), } impl TileOutput { /// Create an output that can produce the input value when this output is selected pub const fn single(value: usize) -> Self { TileOutput::Single(value) } /// Create an output that can produce any of these input values when this output is selected pub const fn any(value: Vec<usize>) -> Self { TileOutput::Random(value) } /// Produce the value this output represents. Will use a default randomly seeded RNG to /// select from a list, if appropriate #[cfg(feature = "impl_fastrand")] pub fn resolve(&self) -> Option<usize> { self.resolve_with(&fastrand::Rng::default()) } /// Produce the value this output represents. Will use a default randomly seeded RNG to /// select from a list, if appropriate #[cfg(feature = "impl_fastrand")] pub fn resolve_with(&self, rng: &fastrand::Rng) -> Option<usize> { match self { Self::Skip => None, Self::Single(val) => Some(*val), Self::Random(vals) => vals.get(rng.usize(0..vals.len())).copied(), } } } /// Checks tile layouts against a matcher instance, and uses the output to produce a value #[derive(Clone, Debug, Default)] pub struct AutoTileRule { /// The pattern that this rule will use for matching pub matcher: TileMatcher, /// The value produced when this rule gets matched pub output: TileOutput, /// When used as part of a set of rules, this value (0.0 - 1.0) determines the chance that /// a successful match will generate an output from this rule pub chance: f32, } impl AutoTileRule { /// Create a rule that will always produce `output_value` when the target tile matches /// `input_value` pub const fn exact(input_value: usize, output_value: usize) -> Self { Self::exact_chance(input_value, output_value, 1.0) } /// Create a rule that will produce `output_value` when the target tile matches /// `input_value` and the selection chance is rolled under the value of `chance` (0.0 to 1.0) pub const fn exact_chance(input_value: usize, output_value: usize, chance: f32) -> Self { AutoTileRule { matcher: TileMatcher::single(input_value), output: TileOutput::single(output_value), chance, } } /// Create a rule that will always produce `output_value` when `matcher` evaluates to /// `true` pub const fn single_when(matcher: TileMatcher, output_value: usize) -> Self { AutoTileRule { matcher, output: TileOutput::single(output_value), chance: 1.0, } } /// Create a rule that will always produce one of the values contained in `output_value` /// when the target tile matches `input_value` pub const fn single_any(input_value: usize, output_value: Vec<usize>) -> Self { Self::single_any_chance(input_value, output_value, 1.0) } /// Create a rule that will produce one of the values contained in `output_value` /// when the target tile matches `input_value` and the selection chacne is rolled under the /// value of `chance` (0.0 to 1.0) pub const fn single_any_chance( input_value: usize, output_value: Vec<usize>, chance: f32, ) -> Self { AutoTileRule { matcher: TileMatcher::single(input_value), output: TileOutput::any(output_value), chance, } } /// Create a rule that will produce one of the values contained in `output_value` /// when when `matcher` evaluates to `true` and the selection chacne is rolled under /// the value of `chance` (0.0 to 1.0) pub const fn any_any_chance( input_value: TileMatcher, output_value: Vec<usize>, chance: f32, ) -> Self { AutoTileRule { matcher: input_value, output: TileOutput::any(output_value), chance, } } /// Evaluate this rule and return the unresolved output value. "None" represents either no /// match or a match that failed its chance roll. /// /// Will use a default randomly seeded RNG to evaluate the chance roll for this rule #[cfg(feature = "impl_fastrand")] pub fn get_match(&self, input: &TileLayout) -> Option<&TileOutput> { let chance = fastrand::f32(); if chance <= self.chance && self.matcher.matches(input) { Some(&self.output) } else { None } } /// Evaluate this rule and return the unresolved output value. "None" represents either no /// match or a match that failed its chance roll. /// /// Will use the provided RNG to evaluate the chance roll for this rule #[cfg(feature = "impl_fastrand")] pub fn get_match_seeded( &self, input: &TileLayout, seeded: &fastrand::Rng, ) -> Option<&TileOutput> { let chance = seeded.f32(); if chance <= self.chance && self.matcher.matches(input) { Some(&self.output) } else { None } } /// Evaluate this rule and produce an output, if a match is found. "None" represents either /// no match, a match that resolved to `TileOutput::Skip`, or a match that failed its chance /// roll. /// /// Will use a default randomly seeded RNG to select from a list, if the output resolves to /// a random selection #[cfg(feature = "impl_fastrand")] pub fn resolve_match(&self, input: &TileLayout) -> Option<usize> { self.get_match(input).and_then(|out| out.resolve()) } /// Evaluate this rule and produce an output, if a match is found. "None" represents either /// no match, a match that resolved to `TileOutput::Skip`, or a match that failed its chance /// roll. /// /// Will use a the provided RNG to select from a list, if the output resolves to /// a random selection #[cfg(feature = "impl_fastrand")] pub fn resolve_match_seeded( &self, input: &TileLayout, seeded: &fastrand::Rng, ) -> Option<usize> { self.get_match_seeded(input, seeded) .and_then(|out| out.resolve_with(seeded)) } } /// Holds a list of rules, for efficiently evaluating a tile layout against multiple exclusive rules. /// Rules will be evaluated in the order they are added to the set, and will stop evaluating when /// a match is found #[derive(Clone, Debug, Default)] pub struct AutoRuleSet(pub Vec<AutoTileRule>); impl Add<AutoRuleSet> for AutoRuleSet { type Output = AutoRuleSet; /// Combine two AutoRuleSet values, where the rules in the right hand side /// will be appended to the end of the set represented by the left hand /// side fn add(self, rhs: AutoRuleSet) -> Self::Output { AutoRuleSet([self.0.as_slice(), rhs.0.as_slice()].concat()) } } impl From<AutoTileRule> for AutoRuleSet { /// Create a rule set from a single rule /// /// ```rust /// # use micro_autotile::{AutoRuleSet, AutoTileRule}; /// # fn main() { /// use micro_autotile::TileLayout; /// let rule_set: AutoRuleSet = AutoTileRule::exact(1, 2).into(); /// /// assert_eq!(rule_set.resolve_match(&TileLayout::single(1)), Some(2)); /// # } /// ``` fn from(value: AutoTileRule) -> Self { Self(vec![value]) } } impl From<Vec<AutoTileRule>> for AutoRuleSet { /// Convert a set of rules into a rule set /// /// ```rust /// # use micro_autotile::{AutoRuleSet, AutoTileRule}; /// # fn main() { /// use micro_autotile::TileLayout; /// let rule_set: AutoRuleSet = vec![ /// AutoTileRule::exact(1, 2), /// AutoTileRule::exact(5123, 231) /// ].into(); /// /// assert_eq!(rule_set.resolve_match(&TileLayout::single(1)), Some(2)); /// # } /// ``` fn from(value: Vec<AutoTileRule>) -> Self { Self(value) } } impl AutoRuleSet { /// Evaluate this set of rules and return the unresolved output value from the first match. /// A return value of `None` means that no rules have matched. /// /// Will use a default randomly seeded RNG to evaluate the chance roll for each matching rule #[cfg(feature = "impl_fastrand")] pub fn get_match(&self, input: &TileLayout) -> Option<&TileOutput> { for rule in self.0.iter() { let result = rule.get_match(input); if result.is_some() { return result; } } None } /// Evaluate this set of rules and return the unresolved output value from the first match. /// A return value of `None` means that no rules have matched, or all matching results failed /// their chance roll or resolved to `TileOutput::Skip`. /// /// Will use the provided RNG to evaluate the chance roll for each matching rule #[cfg(feature = "impl_fastrand")] pub fn get_match_seeded( &self, input: &TileLayout, seeded: &fastrand::Rng, ) -> Option<&TileOutput> { for rule in self.0.iter() { let result = rule.get_match_seeded(input, seeded); if result.is_some() { return result; } } None } /// Evaluate this set of rules and produce an output, if a match is found. /// A return value of `None` means that no rules have matched, or all matching results failed /// their chance roll or resolved to `TileOutput::Skip`. /// /// Will use a default randomly seeded RNG to select from a list, if the output resolves to /// a random selection #[cfg(feature = "impl_fastrand")] pub fn resolve_match(&self, input: &TileLayout) -> Option<usize> { self.get_match(input).and_then(|out| out.resolve()) } /// Evaluate this set of rules and produce an output, if a match is found. /// A return value of `None` means that no rules have matched, or all matching results failed /// their chance roll or resolved to `TileOutput::Skip`. /// /// Will use the provided RNG to select from a list, if the output resolves to /// a random selection #[cfg(feature = "impl_fastrand")] pub fn resolve_match_seeded( &self, input: &TileLayout, seeded: &fastrand::Rng, ) -> Option<usize> { self.get_match_seeded(input, seeded) .and_then(|out| out.resolve_with(seeded)) } }