Skip to main content

msr/
validate.rs

1//! A pure reference validator: replay a record under the Morpion Solitaire rules
2//! and confirm every move is legal.
3//!
4//! This is correctness-first, not speed-first: it uses a plain point set and a
5//! linear scan of drawn lines, mirroring the rules directly rather than the
6//! bit-parallel structures a solver would use. That makes it a trustworthy
7//! reference for the MSR standard, independent of any particular engine.
8
9use crate::cross::initial_cross;
10use crate::model::{Direction, Record};
11use std::collections::HashSet;
12
13/// Why a record failed validation. Each variant carries the offending move's
14/// index (0-based) in `record.moves`.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ValidationError {
17    /// `pos` is outside `0..line_len`.
18    BadLinePosition {
19        /// Offending move index (0-based).
20        index: usize,
21        /// The out-of-range line position.
22        pos: u8,
23    },
24    /// The new point is already occupied.
25    PointOccupied {
26        /// Offending move index (0-based).
27        index: usize,
28        /// The already-occupied point.
29        point: (i16, i16),
30    },
31    /// A non-new point of the line is not present on the board.
32    MissingPoint {
33        /// Offending move index (0-based).
34        index: usize,
35        /// The missing line point.
36        point: (i16, i16),
37    },
38    /// The line breaks the touch rule against an earlier collinear line.
39    TouchRule {
40        /// Offending move index (0-based).
41        index: usize,
42    },
43}
44
45impl std::fmt::Display for ValidationError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            ValidationError::BadLinePosition { index, pos } => {
49                write!(f, "move {}: line position {pos} out of range", index + 1)
50            }
51            ValidationError::PointOccupied { index, point } => {
52                write!(f, "move {}: point {point:?} already occupied", index + 1)
53            }
54            ValidationError::MissingPoint { index, point } => {
55                write!(f, "move {}: line point {point:?} is not present", index + 1)
56            }
57            ValidationError::TouchRule { index } => {
58                write!(f, "move {}: violates the touch rule", index + 1)
59            }
60        }
61    }
62}
63
64impl std::error::Error for ValidationError {}
65
66/// Track key + position of a line, used for the touch rule. Two lines conflict
67/// only if collinear (same direction and same track); `position` then measures
68/// their offset along that track.
69fn track_position(origin: (i16, i16), dir: Direction) -> (i16, i16) {
70    let (x, y) = origin;
71    match dir {
72        Direction::H => (y, x),
73        Direction::V => (x, y),
74        Direction::DP => (x + y, x), // x + y constant along (1,-1)
75        Direction::DN => (x - y, x), // x - y constant along (1,1)
76    }
77}
78
79/// Validate `record`: replay its moves from the initial cross and check each is
80/// a legal Morpion move. Returns the first violation, or `Ok(())`.
81///
82/// Legality of a move is: its `pos` is in range; the new point is currently
83/// empty; the line's other `line_len - 1` points are already present; and the
84/// line does not break the touch rule against any earlier collinear line. The
85/// touch rule forbids two same-direction collinear lines whose track offset is
86/// at most `line_len - 1 - max_overlap`, where `max_overlap` is 1 for touching
87/// variants and 0 for disjoint ones.
88pub fn validate(record: &Record) -> Result<(), ValidationError> {
89    let n = record.variant.line_len();
90    let max_overlap: i16 = if record.variant.disjoint() { 0 } else { 1 };
91    let forbid = n as i16 - 1 - max_overlap;
92
93    let mut points: HashSet<(i16, i16)> = initial_cross(record.variant).into_iter().collect();
94    let mut lines: Vec<((i16, i16), Direction)> = Vec::with_capacity(record.moves.len());
95
96    for (index, m) in record.moves.iter().enumerate() {
97        if m.pos >= n {
98            return Err(ValidationError::BadLinePosition { index, pos: m.pos });
99        }
100        let new_point = (m.x, m.y);
101        if points.contains(&new_point) {
102            return Err(ValidationError::PointOccupied {
103                index,
104                point: new_point,
105            });
106        }
107        // Every other point of the line must already be present.
108        for p in m.line_points(n) {
109            if p != new_point && !points.contains(&p) {
110                return Err(ValidationError::MissingPoint { index, point: p });
111            }
112        }
113        // Touch rule against earlier collinear lines.
114        let origin = m.origin();
115        let (track, position) = track_position(origin, m.dir);
116        for (lo, ld) in &lines {
117            if *ld == m.dir {
118                let (t2, p2) = track_position(*lo, *ld);
119                if t2 == track && (position - p2).abs() <= forbid {
120                    return Err(ValidationError::TouchRule { index });
121                }
122            }
123        }
124        points.insert(new_point);
125        lines.push((origin, m.dir));
126    }
127    Ok(())
128}