diff --git a/README.md b/README.md
index 0b4eb69fdc6cf5bdd056638234257b09430c301a..8c86350daf779c3e5cc14c07417d67bc037fe008 100644
--- a/README.md
+++ b/README.md
@@ -42,8 +42,8 @@ index in some theoretical sprite sheet.
 chance for that rule to be chosen. 
    ```rust
    use micro_autotile::AutoTileRule;
-   const GROUND: usize = 0;
-   const WALL: usize = 1;
+   const GROUND: usize = 1;
+   const WALL: usize = 2;
    
    let alt_ground_rule = AutoTileRule::single_any_chance(GROUND, vec![123, 124, 125], 0.2);
    let fallback_ground_rule = AutoTileRule::exact(GROUND, 126);
diff --git a/src/autotile.rs b/src/autotile.rs
index 9ecbdb010f89418c8e99139c45967c327c28b622..14d6c24bb8d5a944d151008cd3d79f9c6bf22b9b 100644
--- a/src/autotile.rs
+++ b/src/autotile.rs
@@ -1,9 +1,10 @@
+use std::fmt::{Debug, Formatter};
 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)]
+#[derive(Clone, Default)]
 pub struct AutoTileRule {
 	/// The pattern that this rule will use for matching
 	pub matcher: TileMatcher,
@@ -14,6 +15,17 @@ pub struct AutoTileRule {
 	pub chance: f32,
 }
 
+impl Debug for AutoTileRule {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		if f.alternate() {
+			// Perform grid style formatting for matcher value
+			write!(f, "AutoTileRule \n{:#?}output: {:?}, chance: {:.2}", self.matcher, self.output, self.chance)
+		} else {
+			write!(f, "AutoTileRule {{ matcher: {:?}, output: {:?}, chance: {:.2} }}", self.matcher, self.output, self.chance)
+		}
+	}
+}
+
 impl AutoTileRule {
 	/// Create a rule that will always produce `output_value` when the target tile matches
 	/// `input_value`
@@ -25,7 +37,7 @@ impl AutoTileRule {
 	/// `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),
+			matcher: TileMatcher::single_match(input_value),
 			output: TileOutput::single(output_value),
 			chance,
 		}
@@ -56,7 +68,7 @@ impl AutoTileRule {
 		chance: f32,
 	) -> Self {
 		AutoTileRule {
-			matcher: TileMatcher::single(input_value),
+			matcher: TileMatcher::single_match(input_value),
 			output: TileOutput::any(output_value),
 			chance,
 		}
diff --git a/src/layout.rs b/src/layout.rs
index afef824833414b49bba1c0e3435f00f234107c1c..03686d21a4f507211ff93303b168d7c487993f2b 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1,5 +1,24 @@
+use std::fmt::{write, Debug, Formatter};
+use crate::utils::IntoTile;
+
+/// The size of the grid that can be matched; equal to the length of one side of the square grid
+const RULE_MAGNITUDE: usize = 7;
+/// Number of array entries required to store all the data for a grid
+const TILE_GRID_SIZE: usize = RULE_MAGNITUDE.pow(2);
+/// The index of the center tile in the grid
+const GRID_CENTER: usize = (TILE_GRID_SIZE - 1) / 2;
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub struct NotSquareError;
+impl std::fmt::Display for NotSquareError {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		write!(f, "Input is not a square grid")
+	}
+}
+impl std::error::Error for NotSquareError {}
+
 /// Represents how a single tile location should be matched when evaluating a rule
-#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default, Copy, Clone)]
+#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Default, Copy, Clone)]
 pub enum TileStatus {
 	/// This tile will always match, regardless of the input tile
 	#[default]
@@ -14,15 +33,29 @@ pub enum TileStatus {
 	IsNot(i32),
 }
 
+impl Debug for TileStatus {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Self::Ignore => write!(f, "Any Or No Tile"),
+            Self::Nothing => write!(f, "No Tile"),
+            Self::Anything => write!(f, "Any Tile"),
+            Self::Is(value) => write!(f, "Must Match [{}]", value),
+            Self::IsNot(value) => write!(f, "Must Not Match [{}]", value),
+		}
+	}
+}
+
 impl PartialEq<Option<i32>> for TileStatus {
 	fn eq(&self, other: &Option<i32>) -> bool {
-		match self {
+		let matched = match self {
 			Self::Ignore => true,
 			Self::Nothing => other.is_none(),
 			Self::Anything => other.is_some(),
 			Self::Is(value) => &Some(*value) == other,
 			Self::IsNot(value) => &Some(*value) != other,
-		}
+		};
+
+		matched
 	}
 }
 
@@ -41,22 +74,24 @@ impl TileStatus {
 
 /// 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]);
+pub struct TileLayout(pub [Option<i32>; TILE_GRID_SIZE]);
 
 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])
+		let mut grid = [None; TILE_GRID_SIZE];
+		grid[GRID_CENTER] = Some(value);
+		TileLayout(grid)
 	}
 
-	/// Construct a filled 3x3 grid of tile data
-	pub fn filled(values: [i32; 9]) -> Self {
+	/// Construct a filled grid of tile data
+	pub fn filled(values: [i32; TILE_GRID_SIZE]) -> Self {
 		TileLayout(values.map(Some))
 	}
 
-	/// Construct a filled 3x3 grid of identical tile data
+	/// Construct a filled grid of identical tile data
 	pub fn spread(value: i32) -> Self {
-		TileLayout([Some(value); 9])
+		TileLayout([Some(value); TILE_GRID_SIZE])
 	}
 
 	/// Filter the layout data so that it only contains the tiles surrounding the target tile. This
@@ -83,53 +118,88 @@ impl TileLayout {
 	}
 }
 
-/// 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)]
+impl Debug for TileLayout {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		if f.alternate() {
+			let lines = as_lines(&self.0);
+			let mut max_width = 1;
+
+			for line in lines.iter() {
+            	for value in line.iter() {
+					if let Some(value) = value {
+						max_width = max_width.max(value.to_string().len());
+					}
+				}
+			}
+
+			for line in lines {
+				for value in line {
+					if let Some(value) = value {
+						write!(f, "{:^max_width$?} ", value)?;
+					} else {
+						write!(f, "{:^max_width$} ", "#")?;
+					}
+                }
+                write!(f, "\n")?;
+			}
+			write!(f, "\n")
+		} else {
+			writeln!(f, "{:?}", self.0)
+		}
+	}
+}
+
+impl <T> TryFrom<&[T]> for TileLayout where T: IntoTile + Copy + Default + Debug {
+	type Error = NotSquareError;
+	fn try_from(value: &[T]) -> Result<Self, Self::Error> {
+		if is_square(value.len()) {
+			let formatted = transpose(value);
+			Ok(Self(formatted.map(|t| if t.into_tile() == 0 { None } else { Some(t.into_tile()) })))
+		} else {
+			Err(NotSquareError)
+		}
+	}
+}
+
+/// Holds parsed tile rules that are used to evaluate a layout
+#[derive(Clone, Copy)]
 #[repr(transparent)]
-pub struct TileMatcher(pub [TileStatus; 9]);
+pub struct TileMatcher(pub [TileStatus; TILE_GRID_SIZE]);
+impl Debug for TileMatcher {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		if f.alternate() {
+			let lines = as_lines(&self.0);
+			writeln!(f, "Tile Matcher")?;
+			for line in lines {
+				for value in line {
+					write!(f, "{:?} ", value)?;
+				}
+				write!(f, "\n")?;
+			}
+			writeln!(f, "\n")
+		} else {
+			write!(f, "TileMatcher({:?})", self.0)
+		}
+	}
+}
+
+impl Default for TileMatcher {
+	fn default() -> Self {
+        TileMatcher([TileStatus::default(); TILE_GRID_SIZE])
+    }
+}
 
 impl TileMatcher {
-	/// 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,
-		])
+	pub const fn single(value: TileStatus) -> Self {
+		let mut rules = [TileStatus::Ignore; TILE_GRID_SIZE];
+		rules[GRID_CENTER] = value;
+		TileMatcher(rules)
 	}
 
+
 	/// Create a 1x1 matcher, with any rule for the target tile
-	pub const fn single_match(value: TileStatus) -> Self {
-		Self([
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-			value,
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-			TileStatus::Ignore,
-		])
+	pub const fn single_match(value: i32) -> Self {
+		Self::single(TileStatus::Is(value))
 	}
 
 	/// Check if the given input layout of tile data conforms to this matcher
@@ -140,22 +210,165 @@ impl TileMatcher {
 			.all(|(status, reality)| *status == *reality)
 	}
 
-	/// Load data from an LDTK JSON file. Currently supports 1x1 and 3x3 matchers.
+	/// Load data from an LDTK JSON file. Supports arbitrary sized matchers for any square grid.
 	/// Other sizes of matcher will result in `None`
 	pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> {
-		if 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),
-			))
+		if is_square(value.len()) {
+			Some(Self(transpose(value.as_slice()).map(TileStatus::from)))
 		} else {
 			None
 		}
 	}
+}
+
+impl <T> TryFrom<&[T]> for TileMatcher where T: IntoTile + Copy + Default {
+	type Error = NotSquareError;
+	fn try_from(value: &[T]) -> Result<Self, Self::Error> {
+		if is_square(value.len()) {
+			let formatted = transpose(value);
+			Ok(Self(formatted.map(TileStatus::from)))
+		} else {
+			Err(NotSquareError)
+		}
+	}
+}
+impl TryFrom<&[TileStatus]> for TileMatcher {
+	type Error = NotSquareError;
+	fn try_from(value: &[TileStatus]) -> Result<Self, Self::Error> {
+		if is_square(value.len()) {
+            Ok(Self(transpose(value)))
+        } else {
+            Err(NotSquareError)
+        }
+	}
+}
+
+/// Convert a square grid of arbitrary size into one of the expected autotiler grid size (7x7). Odd
+/// numbered grids will be centered in the output, while even numbered grids will be offset by 1 to
+/// the top and left of the output.
+///
+/// This method will panic if the provided list of values is not a square grid
+///
+/// ## Example
+///
+/// For a 2x2 input grid:
+///
+/// ```text
+/// 2 2
+/// 2 2
+/// ```
+///
+/// Applying this function will result in the following output grid:
+///
+/// ```text
+/// 1 1 1 1 1 1 1
+/// 1 1 1 1 1 1 1
+/// 1 1 2 2 1 1 1
+/// 1 1 2 2 1 1 1
+/// 1 1 1 1 1 1 1
+/// 1 1 1 1 1 1 1
+/// 1 1 1 1 1 1 1
+/// ```
+///
+/// ## Example
+///
+/// For a 3x3 input grid:
+///
+/// ```text
+/// 3 3 3
+/// 3 3 3
+/// 3 3 3
+/// ```
+///
+/// Applying this function will result in the following output grid:
+///
+/// ```text
+/// 1 1 1 1 1 1 1
+/// 1 1 1 1 1 1 1
+/// 1 1 3 3 3 1 1
+/// 1 1 3 3 3 1 1
+/// 1 1 3 3 3 1 1
+/// 1 1 1 1 1 1 1
+/// 1 1 1 1 1 1 1
+/// ```
+///
+fn transpose<Value>(input: &[Value]) -> [Value; TILE_GRID_SIZE] where Value: Default + Copy {
+	if input.len() == TILE_GRID_SIZE {
+		match input.try_into() {
+			Ok(output) => return output,
+			Err(_) => {
+				// This should never happen since we've already checked the length - just in case,
+				// we want it to fall through and run the rest of the algorithm
+			}
+		}
+	}
+
+	if !is_square(input.len()) {
+		// Length isn't square == it does not represent a square grid
+        panic!("Input must be a square grid");
+	}
+
+	let input_size = (input.len() as f64).sqrt() as usize;
+	let mut output = [Value::default(); TILE_GRID_SIZE];
+
+	let output_start = if input_size < RULE_MAGNITUDE {
+		// Even padding requires our initial x coord to be half the size difference from the edge.
+		// Even sized squares will be offset to by one to the left and top, which is preferable to
+		// rejecting even squares altogether
+		(RULE_MAGNITUDE - input_size) / 2
+	} else {
+		0
+	};
+
+	let input_start = if input_size > RULE_MAGNITUDE {
+		(input_size - RULE_MAGNITUDE) / 2
+	} else {
+		0
+	};
+
+	for x in 0..input_size {
+		for y in 0..input_size {
+			let adjusted_input_x = x + input_start;
+			let adjusted_input_y = y + input_start;
+			let input_idx = adjusted_input_y * input_size + adjusted_input_x;
+
+			let adjusted_output_x = x + output_start;
+			let adjusted_output_y = y + output_start;
+			let output_idx = adjusted_output_y * RULE_MAGNITUDE + adjusted_output_x;
+
+			output[output_idx] = input[input_idx];
+        }
+	}
+
+	output
+}
+
+fn is_square(n: usize) -> bool {
+	let sqrt_n = (n as f64).sqrt() as usize;
+	n == sqrt_n.pow(2)
+}
+
+fn as_lines<T>(input: &[T]) -> Vec<&[T]> {
+	input.chunks(RULE_MAGNITUDE).collect()
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn it_transposes_odd_grids() {
+		let input = [2, 2, 2, 2, 2, 2, 2, 2, 2];
+		let expected = [
+			0, 0, 0, 0, 0, 0, 0,
+			0, 0, 0, 0, 0, 0, 0,
+			0, 0, 2, 2, 2, 0, 0,
+			0, 0, 2, 2, 2, 0, 0,
+			0, 0, 2, 2, 2, 0, 0,
+			0, 0, 0, 0, 0, 0, 0,
+			0, 0, 0, 0, 0, 0, 0,
+		];
+
+		assert_eq!(expected, transpose(&input));
+	}
 }
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index a4320ce05ec3e49d3e46ffea3e188e7c42ccac4b..88c7a72a8d83140332e09ccdb82aa54188378dcd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,8 +46,8 @@
 //! use micro_autotile::{AutoTileRule, AutoRuleSet};
 //!
 //! # fn main() {
-//! const WALL_TILE: i32 = 0;
-//! const GROUND_TILE: i32 = 1;
+//! const WALL_TILE: i32 = 1;
+//! const GROUND_TILE: i32 = 2;
 //!
 //! // 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);
@@ -60,11 +60,12 @@
 //! // 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),
-//! ]);
+//! let layout_3_x_3 = [
+//!     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),
+//! ];
+//! let enclosed_ground = TileLayout::try_from(layout_3_x_3.as_slice()).unwrap();
 //!
 //! assert_eq!(
 //!   match_1_x_1.resolve_match(&enclosed_ground),
@@ -73,7 +74,7 @@
 //!
 //! // 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));
+//! assert!(TileMatcher::single_match(GROUND_TILE).matches(&enclosed_ground));
 //! # }
 //! ```
 //!
@@ -92,21 +93,21 @@
 //!
 //! # fn main() {
 //! use micro_autotile::{TileMatcher, TileStatus};
-//! const WALL_TILE: i32 = 0;
-//! const GROUND_TILE: i32 = 1;
+//! const WALL_TILE: i32 = 1;
+//! const GROUND_TILE: i32 = 2;
 //! const OTHER_TILE: i32 = 342;
 //!
 //! let wall_rules = AutoRuleSet(vec![
-//! 	AutoTileRule::single_when(TileMatcher([ // Top Left Corner
+//! 	AutoTileRule::single_when(TileMatcher::try_from([ // 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
+//! 	].as_slice()).unwrap(), 54),
+//! 	AutoTileRule::single_when(TileMatcher::try_from([ // 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),
+//! 	].as_slice()).unwrap(), 55),
 //! 	// ... Etc
 //! ]);
 //!
@@ -120,15 +121,16 @@
 //! // Easily merge rule sets in an ordered way
 //! let combined_rules = wall_rules + ground_rules;
 //!
-//! let sublayout = TileLayout([
+//! let sublayout = TileLayout::try_from([
 //! 	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),
-//! ]);
+//! ].as_slice()).unwrap();
 //!
 //! // 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));
 //! # }
diff --git a/src/utils.rs b/src/utils.rs
index 5c9c1823761bbf5b6c79de34bce20ed7dd479598..1c04ee7ae06c3f81ec903a8185068668773a247e 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -103,6 +103,23 @@ impl From<TileStatus> for i64 {
     }
 }
 
+impl <T: IntoTile> IntoTile for Option<T> {
+    fn into_tile(self) -> i32 {
+        match self {
+            Some(value) => value.into_tile(),
+            None => 0,
+        }
+    }
+}
+
+impl <T: IntoTile, E> IntoTile for Result<T, E> {
+    fn into_tile(self) -> i32 {
+        match self {
+            Ok(value) => value.into_tile(),
+            Err(_) => 0,
+        }
+    }
+}
 
 impl <I: IntoTile> From<I> for TileStatus {
     fn from(value: I) -> Self {