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