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}