Skip to main content

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}