use crate::utils::IntoTile;
use std::fmt::{Debug, Formatter};
const RULE_MAGNITUDE: usize = 7;
const TILE_GRID_SIZE: usize = RULE_MAGNITUDE.pow(2);
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 Formatter<'_>) -> std::fmt::Result {
write!(f, "Input is not a square grid")
}
}
impl std::error::Error for NotSquareError {}
#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Default, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(into = "i64", from = "i64"))]
pub enum TileStatus {
#[default]
Ignore,
Nothing,
Anything,
Is(i32),
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 {
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)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from="Vec<TileStatus>", into="Vec<TileStatus>"))]
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct TileLayout(pub [Option<i32>; TILE_GRID_SIZE]);
impl TileLayout {
pub fn single(value: i32) -> Self {
let mut grid = [None; TILE_GRID_SIZE];
grid[GRID_CENTER] = Some(value);
TileLayout(grid)
}
pub fn filled(values: [impl IntoTile; TILE_GRID_SIZE]) -> Self {
TileLayout(values.map(|val| Some(val.into_tile())))
}
pub fn spread(value: impl IntoTile) -> Self {
TileLayout([Some(value.into_tile()); TILE_GRID_SIZE])
}
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],
]
}
}
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 value in lines.iter().copied().flatten().flatten() {
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$} ", "#")?;
}
}
writeln!(f)?;
}
writeln!(f)
} else {
writeln!(f, "{:?}", self.0)
}
}
}
impl<T> TryFrom<&[T]> for TileLayout
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(|t| {
if t.into_tile() == 0 {
None
} else {
Some(t.into_tile())
}
})))
} else {
Err(NotSquareError)
}
}
}
impl TryFrom<Vec<TileStatus>> for TileLayout {
type Error = NotSquareError;
fn try_from(value: Vec<TileStatus>) -> Result<Self, Self::Error> {
if is_square(value.len()) {
let formatted = transpose(value.as_slice());
Ok(Self(formatted.map(|t| {
if t.into_tile() == 0 {
None
} else {
Some(t.into_tile())
}
})))
} else {
Err(NotSquareError)
} }
}
impl From<TileLayout> for Vec<TileStatus> {
fn from(value: TileLayout) -> Self {
value.0.iter().map(|val| (*val).into()).collect()
}
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from="Vec<TileStatus>", into="Vec<TileStatus>"))]
#[repr(transparent)]
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)?;
}
writeln!(f)?;
}
writeln!(f)
} else {
write!(f, "TileMatcher({:?})", self.0)
}
}
}
impl Default for TileMatcher {
fn default() -> Self {
TileMatcher([TileStatus::default(); TILE_GRID_SIZE])
}
}
impl TileMatcher {
pub const fn single(value: TileStatus) -> Self {
let mut rules = [TileStatus::Ignore; TILE_GRID_SIZE];
rules[GRID_CENTER] = value;
TileMatcher(rules)
}
pub const fn single_match(value: i32) -> Self {
Self::single(TileStatus::Is(value))
}
pub fn matches(&self, layout: &TileLayout) -> bool {
self.0
.iter()
.zip(layout.0.iter())
.all(|(status, reality)| *status == *reality)
}
pub fn from_ldtk_array(value: Vec<i64>) -> Option<Self> {
Self::try_from(value.as_slice()).ok()
}
}
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)
}
}
}
impl TryFrom<Vec<TileStatus>> for TileMatcher {
type Error = NotSquareError;
fn try_from(value: Vec<TileStatus>) -> Result<Self, Self::Error> {
TileMatcher::try_from(value.as_slice())
}
}
impl From<TileMatcher> for Vec<TileStatus> {
fn from(value: TileMatcher) -> Self {
value.0.to_vec()
}
}
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(_) => {
}
}
}
if !is_square(input.len()) {
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 {
(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));
}
}