chess_startpos_rs/constraint.rs
1//! Constraint primitives and combinators.
2
3use crate::{ColorKind, PieceKind};
4
5/// Colour of a square — the default binary partition used by chess
6/// (and the default type parameter `C` for [`Constraint`] /
7/// [`crate::Problem`]).
8///
9/// For N-way colour partitions, define your own enum and use it as
10/// the `C` type parameter. Any `Copy + Eq + Hash + Debug` type
11/// satisfies [`ColorKind`].
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[non_exhaustive]
15pub enum SquareColor {
16 /// Light-coloured square.
17 Light,
18 /// Dark-coloured square.
19 Dark,
20}
21
22/// Comparison operator for count constraints.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[non_exhaustive]
26pub enum CountOp {
27 /// Equal.
28 Eq,
29 /// Not equal.
30 NotEq,
31 /// Less than or equal.
32 Le,
33 /// Less than.
34 Lt,
35 /// Greater than or equal.
36 Ge,
37 /// Greater than.
38 Gt,
39}
40
41impl CountOp {
42 /// Returns whether `lhs` and `rhs` satisfy this comparison.
43 ///
44 /// Generic over any [`Ord`] type, so the same operator works for
45 /// counts (`usize`) and signed positional offsets (`i32`).
46 #[must_use]
47 pub fn check<T: Ord>(self, lhs: T, rhs: T) -> bool {
48 match self {
49 Self::Eq => lhs == rhs,
50 Self::NotEq => lhs != rhs,
51 Self::Le => lhs <= rhs,
52 Self::Lt => lhs < rhs,
53 Self::Ge => lhs >= rhs,
54 Self::Gt => lhs > rhs,
55 }
56 }
57}
58
59/// A single constraint over an arrangement of pieces.
60///
61/// Primitive constraints test a property of the arrangement;
62/// combinator constraints (`And` / `Or` / `Not`) compose them.
63///
64/// `P` is the piece kind. `C` is the colour kind (defaults to
65/// [`SquareColor`] — the binary light/dark partition).
66#[derive(Clone, Debug, Eq, PartialEq)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[cfg_attr(
69 feature = "serde",
70 serde(bound(
71 serialize = "P: serde::Serialize, C: serde::Serialize",
72 deserialize = "P: serde::Deserialize<'de>, C: serde::Deserialize<'de>"
73 ))
74)]
75#[non_exhaustive]
76pub enum Constraint<P, C = SquareColor> {
77 /// Number of occurrences of `piece` across the arrangement
78 /// satisfies `(op, value)`.
79 Count {
80 /// Piece kind to count.
81 piece: P,
82 /// Comparison operator.
83 op: CountOp,
84 /// Right-hand value.
85 value: usize,
86 },
87 /// Number of occurrences of `piece` on squares of the given colour
88 /// satisfies `(op, value)`. Used for e.g. "bishops on opposite
89 /// colours" by requiring one bishop on each colour.
90 CountOnColor {
91 /// Piece kind to count.
92 piece: P,
93 /// Square colour to count on.
94 color: C,
95 /// Comparison operator.
96 op: CountOp,
97 /// Right-hand value.
98 value: usize,
99 },
100 /// The `piece` kind must occupy the given square index.
101 /// Satisfied if any occurrence of `piece` is at `square`.
102 At {
103 /// Piece kind.
104 piece: P,
105 /// Square index.
106 square: usize,
107 },
108 /// The `piece` kind must NOT occupy the given square index.
109 NotAt {
110 /// Piece kind.
111 piece: P,
112 /// Square index.
113 square: usize,
114 },
115 /// Strict positional ordering: the indexed instances of the listed
116 /// pieces must appear in strictly increasing square order.
117 ///
118 /// `Order(vec![(Rook, 0), (King, 0), (Rook, 1)])` is read as
119 /// `rook[0] < king[0] < rook[1]`, i.e. the king lies strictly
120 /// between the two rooks.
121 ///
122 /// If any `(piece, instance_idx)` in the chain references an
123 /// instance that does not exist in the arrangement (e.g.
124 /// `(Bishop, 2)` when only two bishops were declared via
125 /// `Constraint::Count { Eq, 2 }`), the constraint is
126 /// **unsatisfied** for that arrangement. The chain silently
127 /// fails — it does not panic. For stricter upfront checking,
128 /// call [`crate::Problem::validate`] (or use
129 /// [`crate::ProblemBuilder::try_build`]).
130 Order(Vec<(P, usize)>),
131 /// Relative positional constraint between two specific piece
132 /// instances:
133 ///
134 /// ```text
135 /// (lhs.0[lhs.1].square as i32 - rhs.0[rhs.1].square as i32) op offset
136 /// ```
137 ///
138 /// `Relative { lhs: (King, 0), rhs: (Queen, 0), op: CountOp::Eq, offset: 2 }`
139 /// reads as "the king is exactly 2 squares to the right of the
140 /// queen". Absolute distance `<= k` between two instances can be
141 /// expressed as `And([Relative { op: Le, offset: k }, Relative { op:
142 /// Ge, offset: -k }])` with matching lhs / rhs.
143 ///
144 /// If either `lhs.1` or `rhs.1` references an instance that
145 /// doesn't exist in the arrangement, the constraint is
146 /// **unsatisfied** for that arrangement (same convention as
147 /// [`Constraint::Order`]).
148 ///
149 /// `offset` is `i32` because the difference of two `usize`
150 /// squares is signed. Square indices are cast to `i32` before
151 /// the subtraction, so boards with `num_squares > i32::MAX` are
152 /// not supported by this constraint (chess uses 8).
153 ///
154 /// ```
155 /// use chess_startpos_rs::{chess, Constraint, CountOp};
156 ///
157 /// // King exactly two squares to the right of the queen.
158 /// let _ = Constraint::<chess::Piece>::Relative {
159 /// lhs: (chess::Piece::King, 0),
160 /// rhs: (chess::Piece::Queen, 0),
161 /// op: CountOp::Eq,
162 /// offset: 2,
163 /// };
164 /// ```
165 Relative {
166 /// Left-hand piece instance.
167 lhs: (P, usize),
168 /// Right-hand piece instance.
169 rhs: (P, usize),
170 /// Comparison operator applied to `(lhs - rhs) op offset`.
171 op: CountOp,
172 /// Signed offset on the right-hand side.
173 offset: i32,
174 },
175 /// Logical AND: all child constraints must hold.
176 ///
177 /// `And(vec![])` is vacuously **true** (empty conjunction).
178 /// `Constraint::And(vec![])` is the natural "always-true"
179 /// constraint when you want to construct a problem with no
180 /// filtering beyond the alphabet and counts.
181 And(Vec<Constraint<P, C>>),
182 /// Logical OR: at least one child constraint must hold.
183 ///
184 /// `Or(vec![])` is vacuously **false** (empty disjunction).
185 /// Use [`Constraint::And`] with an empty vector for the
186 /// always-true constraint instead.
187 Or(Vec<Constraint<P, C>>),
188 /// Logical NOT: child constraint must not hold.
189 Not(Box<Constraint<P, C>>),
190}
191
192impl<P: PieceKind, C: ColorKind> Constraint<P, C> {
193 /// Returns whether `arrangement` satisfies this constraint.
194 ///
195 /// `arrangement.len()` and `colors.len()` must agree.
196 #[must_use]
197 pub fn evaluate(&self, arrangement: &[P], colors: &[C]) -> bool {
198 match self {
199 Self::Count { piece, op, value } => {
200 let n = arrangement.iter().filter(|p| *p == piece).count();
201 op.check(n, *value)
202 }
203 Self::CountOnColor {
204 piece,
205 color,
206 op,
207 value,
208 } => {
209 let n = arrangement
210 .iter()
211 .zip(colors.iter())
212 .filter(|(p, c)| *p == piece && *c == color)
213 .count();
214 op.check(n, *value)
215 }
216 Self::At { piece, square } => arrangement.get(*square) == Some(piece),
217 Self::NotAt { piece, square } => arrangement.get(*square) != Some(piece),
218 Self::Relative {
219 lhs,
220 rhs,
221 op,
222 offset,
223 } => {
224 let lhs_pos = nth_position(arrangement, &lhs.0, lhs.1);
225 let rhs_pos = nth_position(arrangement, &rhs.0, rhs.1);
226 match (lhs_pos, rhs_pos) {
227 (Some(l), Some(r)) => {
228 let diff = (l as i32) - (r as i32);
229 op.check(diff, *offset)
230 }
231 _ => false,
232 }
233 }
234 Self::Order(chain) => {
235 let mut positions: Vec<usize> = Vec::with_capacity(chain.len());
236 for (piece_kind, instance_idx) in chain {
237 let occurrence = nth_position(arrangement, piece_kind, *instance_idx);
238 match occurrence {
239 Some(pos) => positions.push(pos),
240 None => return false,
241 }
242 }
243 positions.windows(2).all(|w| w[0] < w[1])
244 }
245 Self::And(children) => children.iter().all(|c| c.evaluate(arrangement, colors)),
246 Self::Or(children) => children.iter().any(|c| c.evaluate(arrangement, colors)),
247 Self::Not(inner) => !inner.evaluate(arrangement, colors),
248 }
249 }
250
251 /// Collects every `Constraint::Count { piece, op: Eq, value }`
252 /// keyed by `piece` from `self` and its top-level `And`-nested
253 /// children. Used by the solver to derive the per-piece counts
254 /// that define the multiset for the fast-path enumeration.
255 pub(crate) fn collect_eq_counts(&self) -> Vec<(P, usize)> {
256 let mut out = Vec::new();
257 self.collect_eq_counts_into(&mut out);
258 out
259 }
260
261 fn collect_eq_counts_into(&self, out: &mut Vec<(P, usize)>) {
262 match self {
263 Self::Count {
264 piece,
265 op: CountOp::Eq,
266 value,
267 } => out.push((*piece, *value)),
268 Self::And(children) => {
269 for c in children {
270 c.collect_eq_counts_into(out);
271 }
272 }
273 _ => {}
274 }
275 }
276
277 /// Walks the constraint tree, invoking `visitor` on every node.
278 /// Used by validation to check that all `piece` / `color` /
279 /// `square` references are consistent with the problem
280 /// declarations.
281 pub(crate) fn walk(&self, visitor: &mut impl FnMut(&Self)) {
282 visitor(self);
283 match self {
284 Self::And(children) | Self::Or(children) => {
285 for c in children {
286 c.walk(visitor);
287 }
288 }
289 Self::Not(inner) => inner.walk(visitor),
290 _ => {}
291 }
292 }
293
294 /// Returns a logically-equivalent constraint with redundant
295 /// structure removed.
296 ///
297 /// Every arrangement that satisfies `self` satisfies the
298 /// returned constraint and vice-versa — semantics are preserved
299 /// exactly. The rules applied bottom-up are:
300 ///
301 /// * `And([single])` → `single` (collapse single-child conjunctions).
302 /// * `Or([single])` → `single` (collapse single-child disjunctions).
303 /// * `Not(Not(x))` → `x` (eliminate double negation).
304 /// * `Not(And([]))` → `Or([])` (negation of vacuous truth).
305 /// * `Not(Or([]))` → `And([])` (negation of vacuous falsity).
306 /// * If any `And` child simplifies to `Or([])` (vacuously false),
307 /// the whole `And` becomes `Or([])`.
308 /// * If any `Or` child simplifies to `And([])` (vacuously true),
309 /// the whole `Or` becomes `And([])`.
310 /// * Drop neutral children: `And([])` from inside another `And`,
311 /// `Or([])` from inside another `Or`.
312 /// * Leaf constraints clone as-is.
313 ///
314 /// The method is idempotent: `c.simplify() == c.simplify().simplify()`.
315 ///
316 /// `simplify` does *not* impose editor-friendly semantics —
317 /// `Or([])` remains the empty disjunction (always false). Callers
318 /// that want to treat in-progress empty branches as no-ops should
319 /// rewrite their tree before calling `simplify`.
320 ///
321 /// # Examples
322 ///
323 /// ```
324 /// use chess_startpos_rs::{chess::Piece, Constraint, CountOp};
325 ///
326 /// let leaf = Constraint::Count {
327 /// piece: Piece::King,
328 /// op: CountOp::Eq,
329 /// value: 1,
330 /// };
331 /// // `And([leaf])` collapses to `leaf`.
332 /// assert_eq!(
333 /// Constraint::And(vec![leaf.clone()]).simplify(),
334 /// leaf
335 /// );
336 /// // Double negation eliminates.
337 /// assert_eq!(
338 /// Constraint::Not(Box::new(Constraint::Not(Box::new(leaf.clone())))).simplify(),
339 /// leaf
340 /// );
341 /// // An `Or([])` child collapses the whole `And` to `Or([])`.
342 /// assert_eq!(
343 /// Constraint::And(vec![leaf.clone(), Constraint::Or(vec![])]).simplify(),
344 /// Constraint::<Piece>::Or(vec![]),
345 /// );
346 /// ```
347 #[must_use]
348 pub fn simplify(&self) -> Self {
349 match self {
350 Self::And(children) => {
351 let mut acc: Vec<Self> = Vec::with_capacity(children.len());
352 for c in children {
353 let c = c.simplify();
354 if is_false(&c) {
355 return Self::Or(Vec::new());
356 }
357 if is_true(&c) {
358 continue;
359 }
360 acc.push(c);
361 }
362 if acc.len() == 1 {
363 acc.pop().expect("len == 1")
364 } else {
365 Self::And(acc)
366 }
367 }
368 Self::Or(children) => {
369 let mut acc: Vec<Self> = Vec::with_capacity(children.len());
370 for c in children {
371 let c = c.simplify();
372 if is_true(&c) {
373 return Self::And(Vec::new());
374 }
375 if is_false(&c) {
376 continue;
377 }
378 acc.push(c);
379 }
380 if acc.len() == 1 {
381 acc.pop().expect("len == 1")
382 } else {
383 Self::Or(acc)
384 }
385 }
386 Self::Not(inner) => {
387 let inner = inner.simplify();
388 if is_true(&inner) {
389 Self::Or(Vec::new())
390 } else if is_false(&inner) {
391 Self::And(Vec::new())
392 } else if let Self::Not(grandchild) = inner {
393 *grandchild
394 } else {
395 Self::Not(Box::new(inner))
396 }
397 }
398 leaf => leaf.clone(),
399 }
400 }
401}
402
403#[inline]
404fn is_true<P, C>(c: &Constraint<P, C>) -> bool {
405 matches!(c, Constraint::And(v) if v.is_empty())
406}
407
408#[inline]
409fn is_false<P, C>(c: &Constraint<P, C>) -> bool {
410 matches!(c, Constraint::Or(v) if v.is_empty())
411}
412
413/// Returns the square index of the `instance_idx`-th occurrence of
414/// `piece` in `arrangement`, or `None` if fewer than `instance_idx + 1`
415/// occurrences exist.
416fn nth_position<P: PieceKind>(arrangement: &[P], piece: &P, instance_idx: usize) -> Option<usize> {
417 arrangement
418 .iter()
419 .enumerate()
420 .filter_map(|(i, p)| (p == piece).then_some(i))
421 .nth(instance_idx)
422}
423
424#[cfg(test)]
425mod simplify_tests {
426 use super::*;
427 use crate::chess::Piece;
428
429 fn leaf() -> Constraint<Piece> {
430 Constraint::Count {
431 piece: Piece::King,
432 op: CountOp::Eq,
433 value: 1,
434 }
435 }
436
437 fn leaf2() -> Constraint<Piece> {
438 Constraint::At {
439 piece: Piece::Queen,
440 square: 3,
441 }
442 }
443
444 const fn true_<P, C>() -> Constraint<P, C> {
445 Constraint::And(Vec::new())
446 }
447
448 const fn false_<P, C>() -> Constraint<P, C> {
449 Constraint::Or(Vec::new())
450 }
451
452 #[test]
453 fn leaves_simplify_to_themselves() {
454 assert_eq!(leaf().simplify(), leaf());
455 assert_eq!(leaf2().simplify(), leaf2());
456 }
457
458 #[test]
459 fn empty_and_is_identity() {
460 let c: Constraint<Piece> = true_();
461 assert_eq!(c.simplify(), true_());
462 }
463
464 #[test]
465 fn empty_or_is_identity() {
466 let c: Constraint<Piece> = false_();
467 assert_eq!(c.simplify(), false_());
468 }
469
470 #[test]
471 fn single_child_and_unwraps() {
472 let c = Constraint::And(vec![leaf()]);
473 assert_eq!(c.simplify(), leaf());
474 }
475
476 #[test]
477 fn single_child_or_unwraps() {
478 let c = Constraint::Or(vec![leaf()]);
479 assert_eq!(c.simplify(), leaf());
480 }
481
482 #[test]
483 fn double_negation_eliminates() {
484 let c = Constraint::Not(Box::new(Constraint::Not(Box::new(leaf()))));
485 assert_eq!(c.simplify(), leaf());
486 }
487
488 #[test]
489 fn triple_negation_collapses_to_single() {
490 let c = Constraint::Not(Box::new(Constraint::Not(Box::new(Constraint::Not(
491 Box::new(leaf()),
492 )))));
493 assert_eq!(c.simplify(), Constraint::Not(Box::new(leaf())));
494 }
495
496 #[test]
497 fn not_of_true_is_false() {
498 let c: Constraint<Piece> = Constraint::Not(Box::new(true_()));
499 assert_eq!(c.simplify(), false_());
500 }
501
502 #[test]
503 fn not_of_false_is_true() {
504 let c: Constraint<Piece> = Constraint::Not(Box::new(false_()));
505 assert_eq!(c.simplify(), true_());
506 }
507
508 #[test]
509 fn false_propagates_through_and() {
510 let c = Constraint::And(vec![leaf(), false_()]);
511 assert_eq!(c.simplify(), false_());
512 }
513
514 #[test]
515 fn true_propagates_through_or() {
516 let c = Constraint::Or(vec![leaf(), true_()]);
517 assert_eq!(c.simplify(), true_());
518 }
519
520 #[test]
521 fn neutral_true_drops_from_and() {
522 let c = Constraint::And(vec![leaf(), true_()]);
523 assert_eq!(c.simplify(), leaf());
524 }
525
526 #[test]
527 fn neutral_false_drops_from_or() {
528 let c = Constraint::Or(vec![leaf(), false_()]);
529 assert_eq!(c.simplify(), leaf());
530 }
531
532 #[test]
533 fn nested_editor_worst_case() {
534 // And([leaf, Or([Not(Not(leaf2)), Or([])])])
535 // → And([leaf, Or([leaf2])]) (inner Or([]) drops; double Not folds)
536 // → And([leaf, leaf2]) (single-child Or unwraps)
537 let c = Constraint::And(vec![
538 leaf(),
539 Constraint::Or(vec![
540 Constraint::Not(Box::new(Constraint::Not(Box::new(leaf2())))),
541 false_(),
542 ]),
543 ]);
544 let expected = Constraint::And(vec![leaf(), leaf2()]);
545 assert_eq!(c.simplify(), expected);
546 }
547
548 #[test]
549 fn simplify_is_idempotent() {
550 let trees: [Constraint<Piece>; 6] = [
551 leaf(),
552 Constraint::And(vec![leaf(), Constraint::Not(Box::new(false_()))]),
553 Constraint::Or(vec![false_(), Constraint::And(vec![leaf2()])]),
554 Constraint::Not(Box::new(Constraint::Not(Box::new(leaf())))),
555 Constraint::And(vec![Constraint::Or(vec![leaf(), false_()])]),
556 true_(),
557 ];
558 for t in trees {
559 let once = t.simplify();
560 let twice = once.simplify();
561 assert_eq!(once, twice, "not idempotent for {t:?}");
562 }
563 }
564
565 #[test]
566 fn deeply_nested_unsatisfiable_propagates_to_root() {
567 // Or([And([leaf, Or([])])]) → Or([Or([])]) → Or([]) (false drops from Or)
568 let c = Constraint::Or(vec![Constraint::And(vec![leaf(), false_()])]);
569 assert_eq!(c.simplify(), false_());
570 }
571
572 #[test]
573 fn nested_truth_propagates_to_root() {
574 // And([Or([Not(Or([])), leaf])]) → And([Or([And([]), leaf])])
575 // → And([And([])]) (true child collapses Or) → And([]) (single child unwrap)
576 let c = Constraint::And(vec![Constraint::Or(vec![
577 Constraint::Not(Box::new(false_())),
578 leaf(),
579 ])]);
580 assert_eq!(c.simplify(), true_());
581 }
582}