Skip to content
Snippets Groups Projects
Verified Commit d4f55411 authored by Louis's avatar Louis :fire:
Browse files

Convert layout rules to work against 7x7 grids; add a number of conversion...

Convert layout rules to work against 7x7 grids; add a number of conversion methods for converting into the correct format; add custom alternate debug renders for grid based items
parent bc09e4cf
No related branches found
No related tags found
No related merge requests found
......@@ -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);
......
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,
}
......
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
......@@ -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));
//! # }
......
......@@ -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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment