diff --git a/Cargo.toml b/Cargo.toml index 7214de5b583bded988042642e4efe1e5b79342c0..e71893a089689c71bd592877a41f873cb8494c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,4 @@ default = ["impl_fastrand"] impl_fastrand = ["dep:fastrand"] [dependencies] -fastrand = { version = "1.8.0", optional = true } \ No newline at end of file +fastrand = { version = "1.9.0", optional = true } \ No newline at end of file diff --git a/src/autotile.rs b/src/autotile.rs new file mode 100644 index 0000000000000000000000000000000000000000..9ecbdb010f89418c8e99139c45967c327c28b622 --- /dev/null +++ b/src/autotile.rs @@ -0,0 +1,263 @@ +use std::ops::Add; +use crate::{TileLayout, TileMatcher}; +use crate::output::TileOutput; + +/// 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: i32, output_value: i32) -> 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: i32, output_value: i32, 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: i32) -> 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: i32, output_value: Vec<i32>) -> 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: i32, + output_value: Vec<i32>, + 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 `matcher` evaluates to `true` and the selection chance is rolled under + /// the value of `chance` (0.0 to 1.0) + pub const fn any_any_chance( + input_value: TileMatcher, + output_value: Vec<i32>, + 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<i32> { + 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 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<i32> { + 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; + /// use micro_autotile::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; + /// use micro_autotile::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<i32> { + 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<i32> { + self.get_match_seeded(input, seeded) + .and_then(|out| out.resolve_with(seeded)) + } +} + diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000000000000000000000000000000000000..afef824833414b49bba1c0e3435f00f234107c1c --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,161 @@ +/// 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(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 PartialEq<Option<i32>> for TileStatus { + fn eq(&self, other: &Option<i32>) -> 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 { + #[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>; 9]); + +impl TileLayout { + /// Create a 1x1 grid of tile data + pub fn single(value: i32) -> Self { + TileLayout([None, None, None, None, Some(value), None, None, None, None]) + } + + /// Construct a filled 3x3 grid of tile data + pub fn filled(values: [i32; 9]) -> Self { + TileLayout(values.map(Some)) + } + + /// Construct a filled 3x3 grid of identical tile data + pub fn spread(value: i32) -> Self { + TileLayout([Some(value); 9]) + } + + /// 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], + ] + } +} + +/// 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: i32) -> 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(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), + )) + } else { + None + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1e04e9b60a05bfcb894358ba58a2ff7654d90125..a4320ce05ec3e49d3e46ffea3e188e7c42ccac4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,12 +42,12 @@ //! //! e.g. //! ```rust -//! # use micro_autotile::{AutoTileRule, TileLayout, TileOutput}; +//! # use micro_autotile::{TileLayout, TileMatcher}; +//! use micro_autotile::{AutoTileRule, AutoRuleSet}; +//! //! # 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; +//! const WALL_TILE: i32 = 0; +//! const GROUND_TILE: i32 = 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); @@ -87,12 +87,14 @@ //! instances //! //! ```rust -//! # use micro_autotile::{AutoTileRule, AutoRuleSet, TileLayout, TileOutput}; +//! # use micro_autotile::{TileLayout, TileMatcher}; +//! use micro_autotile::{AutoTileRule, AutoRuleSet}; +//! //! # fn main() { //! use micro_autotile::{TileMatcher, TileStatus}; -//! const WALL_TILE: usize = 0; -//! const GROUND_TILE: usize = 1; -//! const OTHER_TILE: usize = 342; +//! const WALL_TILE: i32 = 0; +//! const GROUND_TILE: i32 = 1; +//! const OTHER_TILE: i32 = 342; //! //! let wall_rules = AutoRuleSet(vec![ //! AutoTileRule::single_when(TileMatcher([ // Top Left Corner @@ -132,480 +134,12 @@ //! # } //! ``` -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 - } +mod layout; +mod autotile; +mod output; +mod utils; - /// 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()) - } +pub use layout::*; +pub use autotile::*; +pub use output::*; - /// 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)) - } -} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000000000000000000000000000000000000..b5704d1bd65dcd589fc12595869ec82392dbc37b --- /dev/null +++ b/src/output.rs @@ -0,0 +1,44 @@ +/// 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(i32), + /// Some method should be used to select one of the values in this list + Random(Vec<i32>), +} + +impl TileOutput { + /// Create an output that can produce the input value when this output is selected + pub const fn single(value: i32) -> 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<i32>) -> 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<i32> { + 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<i32> { + match self { + Self::Skip => None, + Self::Single(val) => Some(*val), + Self::Random(vals) => vals.get(rng.usize(0..vals.len())).copied(), + } + } +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..5c9c1823761bbf5b6c79de34bce20ed7dd479598 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,123 @@ +use crate::TileStatus; + +pub trait IntoTile { + fn into_tile(self) -> i32; +} + +// Implementations for unsigned integer types +impl IntoTile for u8 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for u16 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for u32 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for u64 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for u128 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for usize { + fn into_tile(self) -> i32 { + self as i32 + } +} + +// Implementations for signed integer types +impl IntoTile for i8 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for i16 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for i32 { + fn into_tile(self) -> i32 { + self + } +} + +impl IntoTile for i64 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for i128 { + fn into_tile(self) -> i32 { + self as i32 + } +} + +impl IntoTile for isize { + fn into_tile(self) -> i32 { + self as i32 + } +} + +// We don't actually implement IntoTile for TileStatus, as this would conflict with a later From impl +impl TileStatus { + pub fn into_tile(self) -> i32 { + match self { + Self::Ignore => 0, + Self::Nothing => -1000001, + Self::Anything => 1000001, + Self::Is(value) => value, + Self::IsNot(value) => -(value), + } + } +} + +impl From<TileStatus> for i32 { + fn from(value: TileStatus) -> Self { + value.into_tile() + } +} + +impl From<TileStatus> for i64 { + fn from(value: TileStatus) -> Self { + value.into_tile() as i64 + } +} + + +impl <I: IntoTile> From<I> for TileStatus { + fn from(value: I) -> Self { + let value = value.into_tile(); + match value { + 0 => Self::Ignore, + 1000001 => Self::Anything, + -1000001 => Self::Nothing, + other => { + if other > 0 { + Self::Is(other.into_tile()) + } else { + Self::IsNot(other.abs().into_tile()) + } + } + } + } +} \ No newline at end of file