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