use crate::{utils::get_n, DominoError, Puzzle};
pub use complexity_class::ComplexityClass;
use formula::{compute_threshold, find_threshold_index};
mod complexity_class;
mod formula;
pub const NUMBER_OF_CLASSES: usize = 3;
pub fn classify_puzzle(puzzle: &Puzzle) -> Result<ComplexityClass, DominoError> {
if puzzle.0.iter().all(|tile| tile.is_none()) {
return Err(DominoError::EmptyPuzzle); }
let n: usize = get_n(puzzle)? as usize;
let l: usize = if n % 2 == 0 {
(n + 1) * (n + 2) / 2
} else {
(n + 1) * (n + 1) / 2
};
let max_hole: usize = if n >= 4 { n + 1 } else { (n + 1) * 2 - 1 };
let holes: Vec<(usize, usize)> = detect_holes(puzzle);
let class = compute_complexity(holes, max_hole, l);
class
}
#[allow(dead_code)]
pub fn tiles_to_remove_range(class: ComplexityClass, n: usize) -> (usize, usize) {
let l = if n % 2 == 0 {
(n + 1) * (n + 2) / 2
} else {
(n + 1) * (n + 1) / 2
} as f32;
let is_planar = n <= 3;
let max_hole = if is_planar {
l - (n as f32 / 2.0).floor()
} else {
l - (n as f32 + 1.0)
};
let (lower_relative, upper_relative) =
inverse_class_mapping(class).expect("The provided class is invalid");
let lower_bound_float = lower_relative * max_hole;
let upper_bound_float = upper_relative * max_hole;
let min_tiles = std::cmp::max(1, lower_bound_float.floor() as usize);
let max_tiles = if upper_bound_float.fract() == 0.0 {
(upper_bound_float as usize).saturating_sub(1)
} else {
upper_bound_float.floor() as usize
};
let mut max_tiles = std::cmp::min(max_tiles, max_hole as usize);
if class == NUMBER_OF_CLASSES {
max_tiles = max_hole as usize;
}
let (min_tiles, max_tiles) = if max_tiles < min_tiles {
(max_tiles, min_tiles)
} else {
(min_tiles, max_tiles)
};
(min_tiles, max_tiles)
}
#[allow(dead_code)]
fn inverse_class_mapping(class: ComplexityClass) -> Option<(f32, f32)> {
if class < 1 || class > NUMBER_OF_CLASSES {
return None;
}
let min_x = if class == 1 {
0.0
} else {
compute_threshold(class.0 - 1)
};
let max_x = compute_threshold(class.0);
Some((min_x, max_x))
}
fn compute_complexity(
holes: Vec<(usize, usize)>,
max_hole: usize,
len: usize,
) -> Result<ComplexityClass, DominoError> {
if holes.is_empty() {
return ComplexityClass::new(0)
}
let absolute_complexity = compute_absolute_complexity(holes.clone(), max_hole, len);
let relative_complexity = absolute_complexity.clamp(0.0, 1.0);
let class = find_threshold_index(relative_complexity);
ComplexityClass::new(class)
}
fn compute_absolute_complexity(holes: Vec<(usize, usize)>, max_hole: usize, len: usize) -> f32 {
if holes.is_empty() {
return 0.0;
}
let number_of_holes_factor = 1.0 / ((holes.len() as f32).powf(0.1));
let length_factor = holes
.clone()
.into_iter()
.map(|hole| {
let mut hole_length: usize = if hole.1 > hole.0 {
hole.1.saturating_sub(hole.0)
} else {
(len - hole.0) + hole.1
};
hole_length = hole_length.saturating_sub(1);
(hole_length as f32 / max_hole as f32).powf(2.0)
})
.sum::<f32>();
number_of_holes_factor * length_factor
}
pub fn detect_holes(puzzle: &Puzzle) -> Vec<(usize, usize)> {
let len = puzzle.0.len();
let mut holes = Vec::new();
let mut maybe_start: Option<usize> = None;
let mut wraps_around = false;
let mut has_some = false;
for i in 0..len {
if puzzle.0[i].is_none() {
if maybe_start.is_none() {
maybe_start = Some(i);
}
} else {
has_some = true; if let Some(start) = maybe_start.take() {
holes.push((start, i));
}
}
}
if let Some(start) = maybe_start {
if !holes.is_empty() && holes[0].0 == 0 {
wraps_around = true;
holes[0] = (start, holes[0].1);
} else {
holes.push((start, len));
}
}
if wraps_around && holes.len() > 1 {
let first = holes.remove(0); let last = holes.pop().unwrap(); holes.insert(0, (last.0, first.1)); }
if !has_some {
return vec![(0, len)];
}
holes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ComplexityClass, DominoError, Puzzle, Tile};
fn mock_puzzle(size: usize, holes: Vec<usize>) -> Puzzle {
let mut puzzle = vec![Some(Tile(0, 0)); size];
for index in holes {
puzzle[index] = None;
}
puzzle.into()
}
fn test_cases() -> Vec<(ComplexityClass, usize, (usize, usize))> {
let mut cases = Vec::new();
for n in [3, 4, 5, 6] {
for class in 1..=NUMBER_OF_CLASSES {
let expected = tiles_to_remove_range(ComplexityClass::new(class).unwrap(), n);
cases.push((ComplexityClass::new(class).unwrap(), n, expected));
}
}
cases
}
#[test]
fn test_tiles_to_remove_range() {
for (class, n, expected) in test_cases() {
assert_eq!(
tiles_to_remove_range(class, n),
expected,
"Failed for class {:?} and n = {}",
class,
n
);
}
}
mod classify_puzzle_tests {
use super::*;
#[test]
fn test_classify_puzzle_empty_puzzle() {
let puzzle = mock_puzzle(8, (0..8).collect()); assert_eq!(classify_puzzle(&puzzle), Err(DominoError::EmptyPuzzle));
}
#[test]
fn test_classify_puzzle_no_holes() {
let puzzle = mock_puzzle(8, vec![]); assert_eq!(classify_puzzle(&puzzle), Err(DominoError::InvalidClass("The complexity class provided is not valid: 0.\nIt should be in the range [1, 3]".to_string())));
}
}
mod detect_holes_tests {
use super::*;
#[test]
fn test_classify_detect_holes_correctly() {
let puzzle = mock_puzzle(8, vec![1, 2, 4]);
let holes = detect_holes(&puzzle);
assert_eq!(holes, vec![(1, 3), (4, 5)]);
}
#[test]
fn test_classify_detect_holes_single_large_hole() {
let puzzle = mock_puzzle(8, vec![3, 4, 5]);
let holes = detect_holes(&puzzle);
assert_eq!(holes, vec![(3, 6)]);
}
#[test]
fn test_classify_detect_holes_wraparound() {
let puzzle = mock_puzzle(8, vec![6, 7, 0, 1]);
let holes = detect_holes(&puzzle);
assert_eq!(holes, vec![(6, 2)]);
}
}
#[cfg(test)]
mod compute_complexity_tests {
use super::*;
fn create_puzzle_with_hole(n: usize, hole_size: usize, hole_start: usize) -> Puzzle {
let total_size = if n % 2 == 0 {
(n + 1) * (n + 2) / 2
} else {
(n + 1) * (n + 1) / 2
};
let mut puzzle = vec![Some(Tile(0, 0)); total_size];
for i in 0..hole_size {
puzzle[(hole_start + i) % total_size] = None;
}
Puzzle(puzzle)
}
#[test]
fn test_compute_complexity_n3_various_holes() {
let n = 3;
let total_size = (n + 1) * (n + 1) / 2;
let max_hole = (n + 1) * 2 - 1;
for hole_size in 2..=max_hole as usize {
let puzzle = create_puzzle_with_hole(n, hole_size, 0);
let holes = detect_holes(&puzzle);
let complexity = compute_complexity(holes, max_hole, total_size);
let expected_abs = (hole_size.saturating_sub(1) as f32 / max_hole as f32).powf(2.0);
let expected_rel = expected_abs.clamp(0.0, 1.0);
let expected_class = find_threshold_index(expected_rel);
assert_eq!(
complexity.ok(),
Some(ComplexityClass(expected_class)),
"Failed for hole_size = {}",
hole_size
);
}
}
#[test]
fn test_compute_complexity_n3_large_hole() {
let n = 3;
let total_size = (n + 1) * (n + 1) / 2;
let max_hole = (n + 1) * 2 - 1;
let puzzle = create_puzzle_with_hole(n, max_hole as usize, 0);
let holes = detect_holes(&puzzle);
let complexity = compute_complexity(holes, max_hole, total_size);
assert!(complexity.is_ok(), "Failed for a single large hole");
}
#[test]
fn test_compute_complexity_n3_multiple_small_holes() {
let n = 3;
let total_size = (n + 1) * (n + 1) / 2;
let max_hole = (n + 1) * 2 - 1;
let puzzle = mock_puzzle(total_size, vec![2, 3, 5]);
let holes = detect_holes(&puzzle);
let complexity = compute_complexity(holes, max_hole, total_size);
assert!(complexity.is_ok(), "Failed for multiple small holes");
}
}
#[test]
fn test_classify_inverse_class_mapping() {
for class_value in 1..=NUMBER_OF_CLASSES {
let class = ComplexityClass::new(class_value).unwrap();
let result = inverse_class_mapping(class);
assert!(result.is_some());
let (min_x, max_x) = result.unwrap();
let expected_min_x = compute_threshold(class_value - 1);
let expected_max_x = compute_threshold(class_value);
assert_eq!(min_x, expected_min_x);
assert_eq!(max_x, expected_max_x);
}
}
}