use std::collections::{HashMap, HashSet};
use std::fmt;
use crate::{ColorKind, Constraint, PieceKind, SquareColor};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ValidationError {
ColorLengthMismatch {
expected: usize,
actual: usize,
},
UnknownPiece,
UnknownColor,
SquareOutOfRange {
square: usize,
num_squares: usize,
},
InstanceOutOfRange {
instance: usize,
declared: usize,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ColorLengthMismatch { expected, actual } => write!(
f,
"square_colors has length {actual}, expected {expected} (= num_squares)",
),
Self::UnknownPiece => f.write_str("constraint references a piece not in the alphabet"),
Self::UnknownColor => {
f.write_str("CountOnColor references a colour not in square_colors")
}
Self::SquareOutOfRange {
square,
num_squares,
} => write!(
f,
"constraint references square {square} but num_squares is {num_squares}",
),
Self::InstanceOutOfRange {
instance,
declared,
} => write!(
f,
"Order / Relative references instance {instance} of a piece declared with count {declared}",
),
}
}
}
impl std::error::Error for ValidationError {}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "P: serde::Serialize, C: serde::Serialize",
deserialize = "P: serde::Deserialize<'de>, C: serde::Deserialize<'de>"
))
)]
pub struct Problem<P: PieceKind, C: ColorKind = SquareColor> {
pub num_squares: usize,
pub square_colors: Vec<C>,
pub pieces: Vec<P>,
pub constraint: Constraint<P, C>,
}
impl<P: PieceKind, C: ColorKind> Problem<P, C> {
pub fn validate(&self) -> Result<(), ValidationError> {
if !self.square_colors.is_empty() && self.square_colors.len() != self.num_squares {
return Err(ValidationError::ColorLengthMismatch {
expected: self.num_squares,
actual: self.square_colors.len(),
});
}
let alphabet: HashSet<P> = self.dedup_alphabet().into_iter().collect();
let counts: HashMap<P, usize> = self.constraint.collect_eq_counts().into_iter().collect();
let check_instance = |p: &P, idx: usize| -> Option<ValidationError> {
counts
.get(p)
.filter(|&&declared| idx >= declared)
.map(|&declared| ValidationError::InstanceOutOfRange {
instance: idx,
declared,
})
};
let mut error: Option<ValidationError> = None;
self.constraint.walk(&mut |c| {
if error.is_some() {
return;
}
match c {
Constraint::Count { piece, .. } if !alphabet.contains(piece) => {
error = Some(ValidationError::UnknownPiece);
}
Constraint::CountOnColor { piece, color, .. } => {
if !alphabet.contains(piece) {
error = Some(ValidationError::UnknownPiece);
} else if !self.square_colors.contains(color) {
error = Some(ValidationError::UnknownColor);
}
}
Constraint::At { piece, square } | Constraint::NotAt { piece, square } => {
if !alphabet.contains(piece) {
error = Some(ValidationError::UnknownPiece);
} else if *square >= self.num_squares {
error = Some(ValidationError::SquareOutOfRange {
square: *square,
num_squares: self.num_squares,
});
}
}
Constraint::Order(chain) => {
for (p, idx) in chain {
if !alphabet.contains(p) {
error = Some(ValidationError::UnknownPiece);
return;
}
if let Some(e) = check_instance(p, *idx) {
error = Some(e);
return;
}
}
}
Constraint::Relative { lhs, rhs, .. }
if !alphabet.contains(&lhs.0) || !alphabet.contains(&rhs.0) =>
{
error = Some(ValidationError::UnknownPiece);
}
Constraint::Relative { lhs, rhs, .. } => {
if let Some(e) = check_instance(&lhs.0, lhs.1) {
error = Some(e);
} else if let Some(e) = check_instance(&rhs.0, rhs.1) {
error = Some(e);
}
}
_ => {}
}
});
match error {
Some(e) => Err(e),
None => Ok(()),
}
}
#[must_use]
pub fn builder() -> ProblemBuilder<P, C> {
ProblemBuilder::new()
}
#[must_use]
pub fn count(&self) -> u64 {
self.iter().count() as u64
}
pub fn iter(&self) -> impl Iterator<Item = Vec<P>> + '_ {
ProblemIter::new(self)
}
#[must_use]
pub fn at(&self, index: u64) -> Option<Vec<P>> {
let idx = usize::try_from(index).ok()?;
self.iter().nth(idx)
}
#[must_use]
pub fn sample(&self, seed: u64) -> Option<Vec<P>> {
let mut rng = fastrand::Rng::with_seed(seed);
let mut chosen: Option<Vec<P>> = None;
for (i, arrangement) in self.iter().enumerate() {
let seen = (i as u64).saturating_add(1);
if rng.u64(0..seen) == 0 {
chosen = Some(arrangement);
}
}
chosen
}
#[must_use]
pub fn with_constraint(&self, c: Constraint<P, C>) -> Self {
let mut next = self.clone();
next.constraint = match next.constraint {
Constraint::And(mut cs) => {
cs.push(c);
Constraint::And(cs)
}
existing => Constraint::And(vec![existing, c]),
};
next
}
fn dedup_alphabet(&self) -> Vec<P> {
let mut seen: HashSet<P> = HashSet::with_capacity(self.pieces.len());
let mut ordered: Vec<P> = Vec::with_capacity(self.pieces.len());
for p in &self.pieces {
if seen.insert(*p) {
ordered.push(*p);
}
}
ordered
}
fn fixed_count_arrangement(&self) -> Option<Vec<P>> {
let alphabet = self.dedup_alphabet();
if alphabet.is_empty() {
return None;
}
let counts = self.constraint.collect_eq_counts();
if counts.is_empty() {
return None;
}
let mut arrangement: Vec<P> = Vec::with_capacity(self.num_squares);
let mut total = 0usize;
for kind in &alphabet {
let count = counts.iter().find(|(p, _)| p == kind).map(|(_, n)| *n)?;
for _ in 0..count {
arrangement.push(*kind);
}
total += count;
}
if total != self.num_squares {
return None;
}
arrangement.sort();
Some(arrangement)
}
}
#[derive(Clone, Debug)]
pub struct ProblemBuilder<P: PieceKind, C: ColorKind> {
num_squares: usize,
square_colors: Vec<C>,
pieces: Vec<P>,
constraints: Vec<Constraint<P, C>>,
}
impl<P: PieceKind, C: ColorKind> Default for ProblemBuilder<P, C> {
fn default() -> Self {
Self::new()
}
}
impl<P: PieceKind, C: ColorKind> ProblemBuilder<P, C> {
#[must_use]
pub fn new() -> Self {
Self {
num_squares: 0,
square_colors: Vec::new(),
pieces: Vec::new(),
constraints: Vec::new(),
}
}
#[must_use]
pub fn squares(mut self, n: usize) -> Self {
self.num_squares = n;
self
}
#[must_use]
pub fn colors(mut self, colors: Vec<C>) -> Self {
self.square_colors = colors;
self
}
#[must_use]
pub fn alternating_colors(mut self, first: C, second: C) -> Self
where
C: Copy,
{
self.square_colors = crate::alternating(self.num_squares, first, second);
self
}
#[must_use]
pub fn uniform_colors(mut self, c: C) -> Self
where
C: Copy,
{
self.square_colors = crate::uniform(self.num_squares, c);
self
}
#[must_use]
pub fn colors_fn<F>(mut self, f: F) -> Self
where
F: FnMut(usize) -> C,
{
self.square_colors = (0..self.num_squares).map(f).collect();
self
}
#[must_use]
pub fn pieces<I: IntoIterator<Item = P>>(mut self, alphabet: I) -> Self {
self.pieces = alphabet.into_iter().collect();
self
}
#[must_use]
pub fn piece(mut self, p: P) -> Self {
self.pieces.push(p);
self
}
#[must_use]
pub fn constraint(mut self, c: Constraint<P, C>) -> Self {
self.constraints.push(c);
self
}
#[must_use]
pub fn build(self) -> Problem<P, C> {
let constraint = match self.constraints.len() {
0 => Constraint::And(Vec::new()),
1 => self.constraints.into_iter().next().expect("len == 1"),
_ => Constraint::And(self.constraints),
};
Problem {
num_squares: self.num_squares,
square_colors: self.square_colors,
pieces: self.pieces,
constraint,
}
}
pub fn try_build(self) -> Result<Problem<P, C>, ValidationError> {
let problem = self.build();
problem.validate()?;
Ok(problem)
}
}
struct ProblemIter<'a, P: PieceKind, C: ColorKind> {
problem: &'a Problem<P, C>,
state: IterState<P>,
}
enum IterState<P: PieceKind> {
Permutation { current: Option<Vec<P>> },
Cartesian {
alphabet: Vec<P>,
index: HashMap<P, usize>,
current: Option<Vec<P>>,
},
}
impl<'a, P: PieceKind, C: ColorKind> ProblemIter<'a, P, C> {
fn new(problem: &'a Problem<P, C>) -> Self {
let state = match problem.fixed_count_arrangement() {
Some(start) => IterState::Permutation {
current: Some(start),
},
None => {
let alphabet = problem.dedup_alphabet();
let current = if alphabet.is_empty() || problem.num_squares == 0 {
None
} else {
Some(vec![alphabet[0]; problem.num_squares])
};
let index = alphabet.iter().enumerate().map(|(i, p)| (*p, i)).collect();
IterState::Cartesian {
alphabet,
index,
current,
}
}
};
Self { problem, state }
}
}
impl<P: PieceKind, C: ColorKind> Iterator for ProblemIter<'_, P, C> {
type Item = Vec<P>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let candidate = match &mut self.state {
IterState::Permutation { current } => {
let candidate = current.take()?;
let mut next = candidate.clone();
if next_permutation(&mut next) {
*current = Some(next);
}
candidate
}
IterState::Cartesian {
alphabet,
index,
current,
} => {
let candidate = current.take()?;
let mut next = candidate.clone();
if next_cartesian(&mut next, alphabet, index) {
*current = Some(next);
}
candidate
}
};
if candidate.len() == self.problem.num_squares
&& self
.problem
.constraint
.evaluate(&candidate, &self.problem.square_colors)
{
return Some(candidate);
}
}
}
}
fn next_permutation<T: Ord>(v: &mut [T]) -> bool {
let n = v.len();
if n < 2 {
return false;
}
let mut i = n - 1;
while i > 0 && v[i - 1] >= v[i] {
i -= 1;
}
if i == 0 {
return false;
}
let mut j = n - 1;
while v[j] <= v[i - 1] {
j -= 1;
}
v.swap(i - 1, j);
v[i..].reverse();
true
}
fn next_cartesian<P: PieceKind>(v: &mut [P], alphabet: &[P], index: &HashMap<P, usize>) -> bool {
if alphabet.is_empty() {
return false;
}
let last = alphabet[alphabet.len() - 1];
for i in (0..v.len()).rev() {
if v[i] != last {
let pos = *index.get(&v[i]).expect("in alphabet");
v[i] = alphabet[pos + 1];
for slot in v.iter_mut().skip(i + 1) {
*slot = alphabet[0];
}
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{alternating, CountOp};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum Tile {
A,
B,
C,
}
fn light_dark(n: usize) -> Vec<SquareColor> {
alternating(n, SquareColor::Light, SquareColor::Dark)
}
fn fixed_counts(items: &[(Tile, usize)]) -> Constraint<Tile> {
Constraint::And(
items
.iter()
.map(|(p, n)| Constraint::Count {
piece: *p,
op: CountOp::Eq,
value: *n,
})
.collect(),
)
}
#[test]
fn count_constraint_drives_piece_counts() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: fixed_counts(&[(Tile::A, 2), (Tile::B, 1)]),
};
assert_eq!(problem.count(), 3);
}
#[test]
fn duplicates_in_pieces_are_deduped() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::A, Tile::B], constraint: fixed_counts(&[(Tile::A, 2), (Tile::B, 1)]),
};
assert_eq!(problem.count(), 3);
}
#[test]
fn at_constraint() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::At {
piece: Tile::A,
square: 0,
},
]),
};
assert_eq!(problem.count(), 2);
}
#[test]
fn unconstrained_cartesian_fallback() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![]),
};
assert_eq!(problem.count(), 8);
}
#[test]
fn unconstrained_with_at_filter() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::At {
piece: Tile::A,
square: 0,
},
};
assert_eq!(problem.count(), 4);
}
#[test]
fn order_constraint_king_between_rooks() {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
enum Piece {
R,
K,
}
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Piece::R, Piece::K],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Piece::R,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Piece::K,
op: CountOp::Eq,
value: 1,
},
Constraint::Order(vec![(Piece::R, 0), (Piece::K, 0), (Piece::R, 1)]),
]),
};
assert_eq!(problem.count(), 1);
}
#[test]
fn order_with_out_of_range_instance_is_unsatisfied() {
let problem = Problem {
num_squares: 4,
square_colors: light_dark(4),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::Order(vec![
(Tile::B, 0),
(Tile::B, 1),
(Tile::B, 2), ]),
]),
};
assert_eq!(problem.count(), 0);
}
#[test]
fn relative_constraint_exact_offset() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::Relative {
lhs: (Tile::A, 0),
rhs: (Tile::B, 0),
op: CountOp::Eq,
offset: 1,
},
]),
};
let arrangements: Vec<Vec<Tile>> = problem.iter().collect();
assert_eq!(
arrangements,
vec![
vec![Tile::B, Tile::A, Tile::C],
vec![Tile::C, Tile::B, Tile::A],
],
);
}
#[test]
fn relative_constraint_absolute_distance_via_and() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::Relative {
lhs: (Tile::A, 0),
rhs: (Tile::B, 0),
op: CountOp::Le,
offset: 1,
},
Constraint::Relative {
lhs: (Tile::A, 0),
rhs: (Tile::B, 0),
op: CountOp::Ge,
offset: -1,
},
]),
};
assert_eq!(problem.count(), 4);
}
#[test]
fn and_or_not_combinators() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::Not(Box::new(Constraint::At {
piece: Tile::A,
square: 0,
})),
Constraint::Or(vec![
Constraint::At {
piece: Tile::B,
square: 0,
},
Constraint::At {
piece: Tile::C,
square: 0,
},
]),
]),
};
assert_eq!(problem.count(), 4);
}
#[test]
fn count_on_color() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3), pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::CountOnColor {
piece: Tile::A,
color: SquareColor::Light,
op: CountOp::Eq,
value: 2,
},
]),
};
assert_eq!(problem.count(), 1);
}
#[test]
fn at_returns_lexicographic_arrangements() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: fixed_counts(&[(Tile::A, 2), (Tile::B, 1)]),
};
assert_eq!(problem.at(0), Some(vec![Tile::A, Tile::A, Tile::B]));
assert_eq!(problem.at(1), Some(vec![Tile::A, Tile::B, Tile::A]));
assert_eq!(problem.at(2), Some(vec![Tile::B, Tile::A, Tile::A]));
assert_eq!(problem.at(3), None);
}
#[test]
fn sample_is_deterministic_and_in_range() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
]),
};
let arrangements: Vec<Vec<Tile>> = problem.iter().collect();
let first = problem.sample(42).expect("non-empty");
let again = problem.sample(42).expect("non-empty");
assert_eq!(first, again);
assert!(arrangements.contains(&first));
}
#[test]
fn sample_returns_none_for_unsatisfiable() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::C,
op: CountOp::Eq,
value: 1,
},
Constraint::At {
piece: Tile::A,
square: 0,
},
Constraint::NotAt {
piece: Tile::A,
square: 0,
},
]),
};
assert_eq!(problem.count(), 0);
assert_eq!(problem.sample(0), None);
}
#[test]
fn count_op_all_six_variants_filter_correctly() {
let make = |op: CountOp, value: usize| Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::Count {
piece: Tile::A,
op,
value,
},
};
assert_eq!(make(CountOp::Eq, 2).count(), 3);
assert_eq!(make(CountOp::NotEq, 2).count(), 5); assert_eq!(make(CountOp::Lt, 2).count(), 4); assert_eq!(make(CountOp::Le, 2).count(), 7); assert_eq!(make(CountOp::Gt, 1).count(), 4); assert_eq!(make(CountOp::Ge, 1).count(), 7); }
#[test]
fn count_constraint_with_inequality_filters() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Ge,
value: 1,
},
Constraint::Count {
piece: Tile::A,
op: CountOp::Le,
value: 2,
},
]),
};
assert_eq!(problem.count(), 6);
}
#[test]
fn count_constraint_inside_or_does_not_activate_fast_path() {
let problem = Problem {
num_squares: 2,
square_colors: light_dark(2),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::Or(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 2,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 2,
},
]),
};
assert_eq!(problem.count(), 2);
}
#[test]
fn empty_alphabet_yields_zero_arrangements() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![],
constraint: Constraint::And(vec![]),
};
assert_eq!(problem.count(), 0);
assert_eq!(problem.at(0), None);
assert_eq!(problem.sample(0), None);
}
#[test]
fn zero_squares_yields_zero_arrangements() {
let problem: Problem<Tile> = Problem {
num_squares: 0,
square_colors: vec![],
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![]),
};
assert_eq!(problem.count(), 0);
assert_eq!(problem.at(0), None);
assert_eq!(problem.sample(0), None);
}
#[test]
fn validate_accepts_consistent_problem() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: fixed_counts(&[(Tile::A, 1), (Tile::B, 2)]),
};
assert!(problem.validate().is_ok());
}
#[test]
fn validate_accepts_empty_colors() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: vec![],
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![]),
};
assert!(problem.validate().is_ok());
}
#[test]
fn validate_rejects_mismatched_color_length() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: vec![SquareColor::Light], pieces: vec![Tile::A],
constraint: Constraint::And(vec![]),
};
assert_eq!(
problem.validate(),
Err(ValidationError::ColorLengthMismatch {
expected: 3,
actual: 1,
}),
);
}
#[test]
fn validate_rejects_unknown_piece() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A], constraint: Constraint::At {
piece: Tile::B,
square: 0,
},
};
assert_eq!(problem.validate(), Err(ValidationError::UnknownPiece));
}
#[test]
fn validate_rejects_unknown_color() {
let problem: Problem<Tile> = Problem {
num_squares: 2,
square_colors: vec![SquareColor::Light, SquareColor::Light],
pieces: vec![Tile::A],
constraint: Constraint::CountOnColor {
piece: Tile::A,
color: SquareColor::Dark,
op: CountOp::Eq,
value: 0,
},
};
assert_eq!(problem.validate(), Err(ValidationError::UnknownColor));
}
#[test]
fn validate_rejects_instance_out_of_range_in_order() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 2,
},
Constraint::Order(vec![(Tile::B, 0), (Tile::B, 1), (Tile::B, 2)]),
]),
};
assert_eq!(
problem.validate(),
Err(ValidationError::InstanceOutOfRange {
instance: 2,
declared: 2,
}),
);
}
#[test]
fn validate_rejects_instance_out_of_range_in_relative() {
let problem: Problem<Tile> = Problem {
num_squares: 2,
square_colors: light_dark(2),
pieces: vec![Tile::A, Tile::B],
constraint: Constraint::And(vec![
Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 1,
},
Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
},
Constraint::Relative {
lhs: (Tile::A, 1), rhs: (Tile::B, 0),
op: CountOp::Eq,
offset: 0,
},
]),
};
assert_eq!(
problem.validate(),
Err(ValidationError::InstanceOutOfRange {
instance: 1,
declared: 1,
}),
);
}
#[test]
fn validate_rejects_count_on_color_when_no_colors_declared() {
let problem: Problem<Tile> = Problem {
num_squares: 2,
square_colors: vec![],
pieces: vec![Tile::A],
constraint: Constraint::CountOnColor {
piece: Tile::A,
color: SquareColor::Light,
op: CountOp::Eq,
value: 1,
},
};
assert_eq!(problem.validate(), Err(ValidationError::UnknownColor));
}
#[test]
fn validate_rejects_square_out_of_range() {
let problem: Problem<Tile> = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A],
constraint: Constraint::At {
piece: Tile::A,
square: 5, },
};
assert_eq!(
problem.validate(),
Err(ValidationError::SquareOutOfRange {
square: 5,
num_squares: 3,
}),
);
}
#[test]
fn validate_walks_into_combinators() {
let problem: Problem<Tile> = Problem {
num_squares: 2,
square_colors: light_dark(2),
pieces: vec![Tile::A],
constraint: Constraint::Or(vec![
Constraint::At {
piece: Tile::A,
square: 0,
},
Constraint::Not(Box::new(Constraint::At {
piece: Tile::B, square: 1,
})),
]),
};
assert_eq!(problem.validate(), Err(ValidationError::UnknownPiece));
}
#[test]
fn try_build_returns_error_for_invalid_problem() {
let result: Result<Problem<Tile>, _> = Problem::builder()
.squares(3)
.colors(vec![SquareColor::Light]) .pieces([Tile::A])
.try_build();
assert!(matches!(
result,
Err(ValidationError::ColorLengthMismatch { .. })
));
}
#[test]
fn builder_colors_fn_assigns_per_index() {
let problem: Problem<Tile> = Problem::builder()
.squares(6)
.colors_fn(|i| {
if i < 3 {
SquareColor::Light
} else {
SquareColor::Dark
}
})
.pieces([Tile::A])
.build();
assert_eq!(problem.square_colors.len(), 6);
assert_eq!(problem.square_colors[0], SquareColor::Light);
assert_eq!(problem.square_colors[5], SquareColor::Dark);
}
#[test]
fn builder_matches_struct_literal_on_chess_960() {
let from_preset = crate::chess::chess_960().into_problem();
use crate::chess::{back_rank_colors, Piece};
let from_builder: Problem<Piece> = Problem::builder()
.squares(8)
.colors(back_rank_colors())
.pieces([
Piece::King,
Piece::Queen,
Piece::Rook,
Piece::Bishop,
Piece::Knight,
])
.constraint(Constraint::Count {
piece: Piece::King,
op: CountOp::Eq,
value: 1,
})
.constraint(Constraint::Count {
piece: Piece::Queen,
op: CountOp::Eq,
value: 1,
})
.constraint(Constraint::Count {
piece: Piece::Rook,
op: CountOp::Eq,
value: 2,
})
.constraint(Constraint::Count {
piece: Piece::Bishop,
op: CountOp::Eq,
value: 2,
})
.constraint(Constraint::Count {
piece: Piece::Knight,
op: CountOp::Eq,
value: 2,
})
.constraint(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Light,
op: CountOp::Eq,
value: 1,
})
.constraint(Constraint::CountOnColor {
piece: Piece::Bishop,
color: SquareColor::Dark,
op: CountOp::Eq,
value: 1,
})
.constraint(Constraint::Order(vec![
(Piece::Rook, 0),
(Piece::King, 0),
(Piece::Rook, 1),
]))
.build();
assert_eq!(from_builder.count(), from_preset.count());
assert_eq!(from_builder.count(), 960);
}
#[test]
fn builder_with_zero_constraints_is_and_empty() {
let problem: Problem<Tile> = Problem::builder()
.squares(3)
.alternating_colors(SquareColor::Light, SquareColor::Dark)
.pieces([Tile::A, Tile::B])
.build();
assert_eq!(problem.count(), 8);
assert!(matches!(problem.constraint, Constraint::And(ref v) if v.is_empty()));
}
#[test]
fn builder_with_single_constraint_unwraps_and() {
let problem: Problem<Tile> = Problem::builder()
.squares(3)
.uniform_colors(SquareColor::Light)
.pieces([Tile::A, Tile::B])
.constraint(Constraint::At {
piece: Tile::A,
square: 0,
})
.build();
assert_eq!(problem.count(), 4);
assert!(matches!(problem.constraint, Constraint::At { .. }));
}
#[cfg(feature = "serde")]
#[test]
fn problem_serde_roundtrip() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B],
constraint: fixed_counts(&[(Tile::A, 1), (Tile::B, 2)]),
};
let json = serde_json::to_string(&problem).expect("serialise");
let back: Problem<Tile> = serde_json::from_str(&json).expect("deserialise");
assert_eq!(back.num_squares, problem.num_squares);
assert_eq!(back.pieces, problem.pieces);
assert_eq!(back.count(), problem.count());
}
#[test]
fn builder_piece_method_appends() {
let problem: Problem<Tile> = Problem::builder()
.squares(3)
.uniform_colors(SquareColor::Light)
.piece(Tile::A)
.piece(Tile::B)
.constraint(Constraint::Count {
piece: Tile::A,
op: CountOp::Eq,
value: 2,
})
.constraint(Constraint::Count {
piece: Tile::B,
op: CountOp::Eq,
value: 1,
})
.build();
assert_eq!(problem.count(), 3);
}
#[test]
fn with_constraint_adds_via_and() {
let problem = Problem {
num_squares: 3,
square_colors: light_dark(3),
pieces: vec![Tile::A, Tile::B, Tile::C],
constraint: fixed_counts(&[(Tile::A, 1), (Tile::B, 1), (Tile::C, 1)]),
};
assert_eq!(problem.count(), 6);
let narrowed = problem.with_constraint(Constraint::At {
piece: Tile::A,
square: 0,
});
assert_eq!(narrowed.count(), 2);
}
}