From 078ba9e0566bfc912948e649c0a98210dc4235c5 Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Mon, 17 Apr 2023 15:23:39 +0100
Subject: [PATCH] Write rustdoc w/ doc tests

---
 .gitignore   |   1 +
 README.md    |  73 +++++++++++
 rustfmt.toml |   4 +
 src/lib.rs   | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 428 insertions(+)
 create mode 100644 README.md
 create mode 100644 rustfmt.toml

diff --git a/.gitignore b/.gitignore
index 4fffb2f..3d84f13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /target
 /Cargo.lock
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9927afa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+# Micro Autotile
+
+[![https://crates.io/crates/micro_autotile](https://img.shields.io/crates/v/micro_autotile?style=for-the-badge)](https://crates.io/crates/micro_autotile)
+[![https://docs.rs/micro_autotile](https://img.shields.io/docsrs/micro_autotile?style=for-the-badge)](https://docs.rs/micro_autotile)
+
+_Bring LDTK's autotile feature to your Rust project_
+
+`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. Great for either building a
+compatible editor into your project, or for using LDTK's rule format to decorate generated 
+content.
+
+## Installation
+
+Either add it to your `Cargo.toml` dependencies:
+
+```toml
+[dependencies]
+micro_autotile = "0.1"
+```
+
+Or use cargo to add it to your project:
+
+```sh
+cargo add micro_autotile
+```
+
+## Usage
+
+_For a more thorough usage guide, check out the [docs.rs](https://docs.rs/micro_autotile) page_
+
+Autotiling is, fundamentally, a method of mapping a two dimensional pattern to a single scalar value.
+The context for this is usually "tile maps" in video games, and while that is not the sole use case it
+is the one that will be assumed throughout the docs for `micro_autotile`.
+
+To start, determine what your input types and output types will be - in this example, we will use integers
+to define types of terrain (walls, ground, water, etc) for our input, and the output will be a specific sprite
+index in some theoretical sprite sheet.
+
+1. Create one or more rules that define a pattern to match, a value to output, and optionally a percentage
+chance for that rule to be chosen. 
+    * ```rust
+      use micro_autotile::AutoTileRule;
+      const GROUND: usize = 0;
+      const WALL: usize = 1;
+      
+      let alt_ground_rule = AutoTileRule::single_any_chance(GROUND, vec![123, 124, 125], 0.2);
+      let fallback_ground_rule = AutoTileRule::exact(GROUND, 126);
+   ```
+2. (Optional) Put together your rules in a rule set. This can be skipped and the rule structs used directly
+    * ```rust
+      use micro_autotile::{AutoRuleSet, AutoTileRule};
+      let ground_rules = AutoRuleSet::new(vec![alt_ground_rule, fallback_ground_rule]);
+      let wall_rules = AutoTileRule::exact(WALL, 35).into();
+      let combined_rules = wall_rules + ground_rules;
+    ```
+3. Elsewhere, generate a slice of level data wrapped in a `TileLayout` struct. This represents a single tile
+   (the central element) and its surrounding neighbors. The order of the neighbors is important, and is laid
+    out as though in a flattened grid.
+    * ```rust
+      use micro_autotile::TileLayout;
+      let layout = TileLayout::filled([
+        GROUND, GROUND, GROUND,
+        GROUND, WALL, GROUND,
+        GROUND, GROUND, GROUND,
+      ]);
+    ```
+4. Produce an output using either the rule set or the rule directly (the same methods exist for both)
+    * ```rust
+      let output = combined_rules.resolve_match(&layout);
+      assert_eq!(output, Some(35));
+    ```
\ No newline at end of file
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..d62aed7
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,4 @@
+hard_tabs = true
+#group_imports = "StdExternalCrate"
+use_field_init_shorthand = true
+use_try_shorthand = true
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index b3fe406..1e04e9b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,12 +1,152 @@
+//! `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),
 }
 
@@ -49,11 +189,26 @@ impl TileStatus {
 	}
 }
 
+/// 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,
@@ -68,6 +223,7 @@ impl TileMatcher {
 		])
 	}
 
+	/// Create a 1x1 matcher, with any rule for the target tile
 	pub const fn single_match(value: TileStatus) -> Self {
 		Self([
 			TileStatus::Ignore,
@@ -82,6 +238,7 @@ impl TileMatcher {
 		])
 	}
 
+	/// Check if the given input layout of tile data conforms to this matcher
 	pub fn matches(&self, layout: &TileLayout) -> bool {
 		self.0
 			.iter()
@@ -89,6 +246,8 @@ impl TileMatcher {
 			.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];
@@ -107,14 +266,43 @@ impl TileMatcher {
 	}
 }
 
+/// 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],
@@ -122,35 +310,72 @@ impl TileLayout {
 	}
 }
 
+/// 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),
@@ -158,6 +383,9 @@ impl AutoTileRule {
 			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,
@@ -166,10 +394,15 @@ impl AutoTileRule {
 		}
 	}
 
+	/// 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>,
@@ -182,6 +415,9 @@ impl AutoTileRule {
 		}
 	}
 
+	/// 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>,
@@ -194,6 +430,10 @@ impl AutoTileRule {
 		}
 	}
 
+	/// 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();
@@ -205,6 +445,10 @@ impl AutoTileRule {
 		}
 	}
 
+	/// 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,
@@ -219,20 +463,94 @@ impl AutoTileRule {
 			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() {
@@ -244,6 +562,11 @@ impl AutoRuleSet {
 		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,
@@ -258,4 +581,31 @@ impl AutoRuleSet {
 		}
 		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))
+	}
 }
-- 
GitLab