minsweeper_rs/solver/
mia.rs

1use crate::board::Point;
2use crate::solver::Operation::{Chord, Flag, Reveal};
3use crate::solver::{Action, Logic, Move, Reason, Solver};
4use crate::{CellState, CellType, GameState, GameStatus};
5use linked_hash_set::LinkedHashSet;
6use std::cmp::Ordering;
7use std::collections::HashSet;
8use std::fmt::{Display, Formatter};
9use std::hash::{Hash, Hasher};
10use std::ops::Sub;
11
12#[derive(Copy, Clone, Debug)]
13pub struct MiaSolver;
14
15impl MiaSolver {
16    const BRUTE_FORCE_LIMIT: usize = 30;
17}
18
19impl Display for MiaSolver {
20    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21        write!(f, "{:?}", self)
22    }
23}
24
25impl Solver for MiaSolver {
26
27    fn solve(&self, state: &GameState) -> Option<Move> {
28
29        let size = state.board.size();
30
31        if state.status != GameStatus::Playing { return None };
32
33        for point in size.points() {
34            let CellType::Safe(number) = state.board[point].cell_type else { continue };
35
36            let mut marked_mines = HashSet::new();
37            let mut empty_spaces = HashSet::new();
38
39            for point in size.neighbours(point) {
40                match state.board[point].cell_state {
41                    CellState::Flagged => {
42                        marked_mines.insert(point);
43                        empty_spaces.insert(point);
44                    }
45                    CellState::Unknown => {
46                        empty_spaces.insert(point);
47                    }
48                    _ => {}
49                }
50            }
51
52            if number as usize == marked_mines.len() && empty_spaces.len() > marked_mines.len() {
53                return Some(Move::single(Action::new(point, Chord), Some(Reason::new(MiaLogic::Chord, marked_mines))))
54            } else if number as usize == empty_spaces.len() {
55                let clicks: HashSet<_> = size.neighbours(point)
56                        .filter(|e| state.board[*e].cell_state == CellState::Unknown)
57                        .map(|e| Action::new(e, Flag))
58                        .collect();
59
60                if !clicks.is_empty() {
61                    return Some(Move::multi(clicks, Some(Reason::new(MiaLogic::FlagChord, empty_spaces))));
62                }
63            } else if (number as usize) < marked_mines.len() {
64                let clicks: HashSet<_> = size.neighbours(point)
65                        .filter(|e| state.board[*e].cell_state == CellState::Flagged)
66                        .map(|e| Action::new(e, Flag))
67                        .collect();
68
69                return Some(Move::multi(clicks, Some(Reason::new(MiaLogic::FlagChord, empty_spaces))));
70            }
71        }
72
73        // hehe logical deduction
74        // i hope this isn't too hateful to implement in Rust
75
76        #[derive(Clone, Debug, Eq, PartialEq)]
77        struct Flag {
78            number: i8,
79            points: HashSet<Point>
80        }
81
82        impl Flag {
83            pub const fn new(number: i8, points: HashSet<Point>) -> Self {
84                Self { number, points }
85            }
86
87            pub fn contains(&self, other: &Self) -> bool {
88                self.number >= other.number
89                        && self.points.is_superset(&other.points)
90            }
91        }
92
93        impl PartialOrd for Flag {
94            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95                if self == other {
96                    return Some(Ordering::Equal)
97                }
98
99                if self.contains(other) {
100                    return Some(Ordering::Greater)
101                }
102
103                if other.contains(self) {
104                    return Some(Ordering::Less)
105                }
106
107                None
108            }
109        }
110
111        impl Sub for &Flag {
112            type Output = Flag;
113
114            fn sub(self, rhs: Self) -> Self::Output {
115                // if !(self >= rhs) {
116                //     panic!("mewo");
117                // }
118
119                let mut points = self.points.clone();
120
121                for point in &rhs.points {
122                    points.remove(point);
123                }
124
125                Flag::new(self.number - rhs.number, points)
126            }
127        }
128
129        impl Hash for Flag {
130            fn hash<H: Hasher>(&self, state: &mut H) {
131                self.number.hash(state);
132                for point in &self.points {
133                    point.hash(state)
134                }
135            }
136        }
137
138        let mut flags = LinkedHashSet::new();
139
140        for point in size.points() {
141            let CellType::Safe(mut required) = state.board[point].cell_type else {
142                continue
143            };
144
145            for point in size.neighbours(point) {
146                if state.board[point].cell_state == CellState::Flagged {
147                    required = required.saturating_sub(1)
148                }
149            }
150
151            if required == 0 {
152                continue
153            }
154
155            let neighbours: HashSet<_> = size.neighbours(point)
156                    .filter(|e| state.board[*e].cell_state == CellState::Unknown)
157                    .collect();
158
159            if neighbours.is_empty() {
160                continue
161            }
162
163            flags.insert(Flag::new(required as i8, neighbours));
164        }
165
166        let mut changed = true;
167        while changed {
168
169            let mut to_add = HashSet::new();
170            for flag in &flags {
171                // entirely contained stuffs
172                {
173                    let contained_flags: Vec<_> = flags.iter()
174                            .filter(|e| flag >= e)
175                            .collect();
176
177                    for contained in contained_flags {
178                        let remaining = flag - contained;
179
180                        if remaining.points.is_empty() {
181                            continue
182                        }
183
184                        if remaining.number == 0 {
185                            return Some(Move::multi(
186                                remaining.points
187                                        .into_iter()
188                                        .map(|e| Action::new(e, Reveal))
189                                        .collect(),
190                                Some(Reason::new(MiaLogic::RegionDeductionReveal, contained.points.clone()))
191                            ))
192                        } else if remaining.number > 0 && remaining.number as usize == remaining.points.len() {
193                            return Some(Move::multi(
194                                remaining.points
195                                        .into_iter()
196                                        .map(|e| Action::new(e, Flag))
197                                        .collect(),
198                                Some(Reason::new(MiaLogic::RegionDeductionFlag, contained.points.clone()))
199                            ))
200
201                        }
202
203                        to_add.insert(remaining);
204                    }
205                }
206
207                // not entirely contained stuffs
208                {
209                    let touching_flags = flags.iter()
210                            .filter(|e| e.points.iter()
211                                    .any(|e| flag.points.contains(e)));
212
213                    for touching in touching_flags {
214                        let remaining = flag - touching;
215
216                        if remaining.points.is_empty() {
217                            continue
218                        }
219
220                        if remaining.number > 0 && remaining.number as usize == remaining.points.len() {
221                            return Some(Move::multi(
222                                remaining.points
223                                        .into_iter()
224                                        .map(|e| Action::new(e, Flag))
225                                        .collect(),
226                                Some(Reason::new(MiaLogic::RegionDeductionFlag, touching.points.clone()))
227                            ))
228                        }
229                    }
230                }
231            }
232
233            changed = to_add.into_iter()
234                    .map(|e| flags.insert(e))
235                    .reduce(|a, b| a || b)
236                    .unwrap_or(false);
237        }
238
239        if state.remaining_mines == 0 {
240            let clicks: HashSet<_> = size.points()
241                    .filter(|e| state.board[*e].cell_state == CellState::Unknown)
242                    .map(|e| Action::new(e, Reveal))
243                    .collect();
244
245            if !clicks.is_empty() {
246                return Some(Move::multi(clicks, Some(Reason::new(MiaLogic::RegionDeductionFlag, HashSet::new()))))
247            }
248        }
249
250        let mut empties = HashSet::new();
251        let mut adjacents = HashSet::new();
252
253        for point in size.points() {
254            if state.board[point].cell_state == CellState::Unknown {
255                for neighbour in size.neighbours(point) {
256                    if matches!(state.board[neighbour].cell_type, CellType::Safe(number) if number > 0) {
257                        empties.insert(point);
258                        adjacents.insert(neighbour);
259                    }
260                }
261            }
262        }
263
264        if empties.len() < Self::BRUTE_FORCE_LIMIT && !adjacents.is_empty() {
265            let states: Vec<GameState> = brute_force(&adjacents.into_iter().collect(), 0, state)
266                    .collect();
267
268            if !states.is_empty() {
269                let mut clicks = HashSet::new();
270
271                for point in empties.iter().copied() {
272                    if states.iter().all(|e| e.board[point].cell_state != CellState::Flagged) {
273                        clicks.insert(Action::new(point, Reveal));
274                    }
275                    if states.iter().all(|e| e.board[point].cell_state == CellState::Flagged) {
276                        clicks.insert(Action::new(point, Flag));
277                    }
278                }
279
280                if !clicks.is_empty() {
281                    return Some(Move::multi(clicks, Some(Reason::new(MiaLogic::BruteForce, empties))))
282                }
283
284                if states.iter().all(|e| e.remaining_mines == 0) {
285                    for point in size.points() {
286                        if state.board[point].cell_state == CellState::Unknown
287                                && states.iter().all(|e| e.board[point].cell_state != CellState::Flagged) {
288                            clicks.insert(Action::new(point, Reveal));
289                        }
290                    }
291                }
292
293                if !clicks.is_empty() {
294                    return Some(Move::multi(clicks, Some(Reason::new(MiaLogic::BruteForceExhaustion, empties))))
295                }
296
297            }
298
299        }
300
301        None
302    }
303}
304
305fn brute_force(points: &Vec<Point>, index: usize, state: &GameState) -> Box<dyn Iterator<Item = GameState>> {
306    let size = state.board.size();
307    let mut empties = vec![];
308    let current = points[index];
309
310    let mut flags = 0;
311
312    let CellType::Safe(number) = state.board[current].cell_type else {
313        unreachable!()
314    };
315
316    for point in size.neighbours(current) {
317        match state.board[point].cell_state {
318            CellState::Unknown => empties.push(point),
319            CellState::Flagged => flags += 1,
320            _ => {}
321        }
322    }
323
324    let mines_to_flag = number as isize - flags;
325
326    if mines_to_flag > state.remaining_mines || mines_to_flag as usize > empties.len() {
327        return Box::new(std::iter::empty())
328    }
329
330    if mines_to_flag == 0 || empties.is_empty() {
331        if index + 1 == points.len() {
332            return Box::new(std::iter::once(state.clone()));
333        }
334        return brute_force(points, index + 1, state);
335    };
336
337    let mut stream: Vec<Box<dyn Iterator<Item = GameState>>> = vec![];
338
339    for flag_combinations in get_flag_combinations(&empties, mines_to_flag) {
340        let mut state_copy = state.clone();
341
342        for point in &empties {
343            if flag_combinations.contains(point) {
344                simulate_right_click(&mut state_copy, *point)
345            } else {
346                simulate_reveal(&mut state_copy, *point)
347            }
348        }
349
350        if index + 1 == points.len() {
351            stream.push(Box::new(std::iter::once(state_copy)))
352        } else {
353            stream.push(Box::new(brute_force(points, index + 1, &state_copy)))
354        }
355    }
356
357    Box::new(stream.into_iter()
358            .flatten())
359}
360
361fn get_flag_combinations(empties: &Vec<Point>, mines_to_flag: isize) -> Vec<HashSet<Point>> {
362    if empties.len() < mines_to_flag as usize {
363        return Vec::new()
364    }
365
366    recursive_get_flag_combinations(HashSet::new(), empties, 0, mines_to_flag)
367            .collect()
368}
369
370fn recursive_get_flag_combinations(selected: HashSet<Point>, empties: &Vec<Point>, start: usize, mines_to_flag: isize) -> Box<dyn Iterator<Item = HashSet<Point>>> {
371    if mines_to_flag < 1 {
372        return Box::new(std::iter::empty())
373    }
374
375    let mut stream: Vec<Box<dyn Iterator<Item = HashSet<Point>>>> = vec![];
376
377    for i in start..empties.len() {
378        let mut selected = selected.clone();
379        selected.insert(empties[i]);
380        if mines_to_flag == 1 {
381            stream.push(Box::new(std::iter::once(selected)))
382        } else {
383            stream.push(recursive_get_flag_combinations(selected, empties, start + 1, mines_to_flag - 1));
384        }
385    }
386
387    Box::new(stream.into_iter()
388            .flatten())
389}
390
391fn simulate_right_click(state: &mut GameState, point: Point) {
392    let cell = &mut state.board[point];
393    match cell.cell_state {
394        CellState::Unknown => {
395            cell.cell_state = CellState::Flagged;
396            state.remaining_mines -= 1;
397        }
398        CellState::Flagged => {
399            cell.cell_state = CellState::Unknown;
400            state.remaining_mines += 1;
401        }
402        CellState::Revealed => unreachable!()
403    }
404}
405
406fn simulate_reveal(state: &mut GameState, point: Point) {
407    // it is normally illegal to have a revealed cell still be unknown
408    // but such are the circumstances we find ourselves in
409    state.board[point].cell_state = CellState::Revealed;
410}
411
412
413#[derive(Copy, Clone, Debug)]
414pub enum MiaLogic {
415    Chord,
416    FlagChord,
417    RegionDeductionReveal,
418    RegionDeductionFlag,
419    ZeroMinesRemaining,
420    BruteForce,
421    BruteForceExhaustion,
422}
423
424impl Display for MiaLogic {
425    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
426        match self {
427            MiaLogic::Chord => write!(f, "the amount of flags around the cell matches its number"),
428            MiaLogic::FlagChord => write!(f, "the amount of flaggable cells around the cell matches its number"),
429            MiaLogic::RegionDeductionReveal => write!(f, "the surrounding cells force the cells to be safe"),
430            MiaLogic::RegionDeductionFlag => write!(f, "the surrounding cells force the cells to be a mine"),
431            MiaLogic::ZeroMinesRemaining => write!(f, "0 mines remaining, all unknown cells must be safe"),
432            MiaLogic::BruteForce => write!(f, "in every possible mine configuration the cells are safe/mines"),
433            MiaLogic::BruteForceExhaustion => write!(f, "in every possible mine configuration every mine is determined, all unused cells must be safe")
434        }
435    }
436}
437
438impl Logic for MiaLogic {
439
440}