1use crate::board::{Board, BoardSize, Point};
2use crate::solver::Operation::{Chord, Flag, Reveal};
3use crate::solver::{Action, Actionable, GameResult, Logic, Move, Reason, Solver};
4use crate::{CellState, CellType, GameState, GameStatus, Minsweeper};
5use std::cmp::Ordering;
6use std::collections::HashSet;
7use std::fmt::{Display, Formatter};
8use std::hash::{Hash, Hasher};
9use std::ops::Sub;
10use enumset::{EnumSet, EnumSetType};
11
12#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
13pub enum Level {
14 Beginner,
15 Intermediate,
16 Expert
17}
18
19impl Level {
20 fn logics(self) -> EnumSet<MiaLogic> {
21 match self {
22 Level::Beginner => MiaLogic::Chord | MiaLogic::FlagChord,
23 Level::Intermediate => MiaLogic::RegionDeductionReveal | MiaLogic::RegionDeductionFlag
24 | MiaLogic::ZeroMinesRemaining,
25 Level::Expert => MiaLogic::BruteForce | MiaLogic::BruteForceExhaustion,
26 }
27 }
28}
29
30#[derive(Copy, Clone, Debug)]
31pub struct MiaSolver {
32 skill_level: Level,
33 required_level: Option<Level>,
34}
35
36impl MiaSolver {
37 const BRUTE_FORCE_LIMIT: usize = 30;
38
39 pub const fn skill(level: Level) -> Self {
40 Self {
41 skill_level: level,
42 required_level: None,
43 }
44 }
45
46 pub const fn only(level: Level) -> Self {
47 Self {
48 skill_level: level,
49 required_level: Some(level),
50 }
51 }
52}
53
54impl Default for MiaSolver {
55 fn default() -> Self {
56 Self::skill(Level::Expert)
57 }
58}
59
60impl Display for MiaSolver {
61 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62 write!(f, "{:?}", self)
63 }
64}
65
66impl MiaSolver {
67 fn internal_solve(&self, state: &GameState) -> Option<(Move, MiaLogic)> {
68
69 let size = state.board.size();
70
71 if state.status != GameStatus::Playing { return None };
72
73 for point in size.points() {
74 let CellType::Safe(number) = state.board[point].cell_type else { continue };
75
76 let mut marked_mines = HashSet::new();
77 let mut empty_spaces = HashSet::new();
78
79 for point in size.neighbours(point) {
80 match state.board[point].cell_state {
81 CellState::Flagged => {
82 marked_mines.insert(point);
83 empty_spaces.insert(point);
84 }
85 CellState::Unknown => {
86 empty_spaces.insert(point);
87 }
88 _ => {}
89 }
90 }
91
92 if number as usize == marked_mines.len() && empty_spaces.len() > marked_mines.len() {
93 return Some((Move::single(Action::new(point, Chord), Some(Reason::new(MiaLogic::Chord, marked_mines))), MiaLogic::Chord))
94 } else if number as usize == empty_spaces.len() {
95 let clicks: HashSet<_> = size.neighbours(point)
96 .filter(|e| state.board[*e].cell_state == CellState::Unknown)
97 .map(|e| Action::new(e, Flag))
98 .collect();
99
100 if !clicks.is_empty() {
101 return Some((Move::multi(clicks, Some(Reason::new(MiaLogic::FlagChord, empty_spaces))), MiaLogic::FlagChord));
102 }
103 } else if (number as usize) < marked_mines.len() {
104 let clicks: HashSet<_> = size.neighbours(point)
105 .filter(|e| state.board[*e].cell_state == CellState::Flagged)
106 .map(|e| Action::new(e, Flag))
107 .collect();
108
109 return Some((Move::multi(clicks, Some(Reason::new(MiaLogic::FlagChord, empty_spaces))), MiaLogic::FlagChord));
110 }
111 }
112
113 if self.skill_level < Level::Intermediate {
114 return None
115 }
116
117 #[derive(Clone, Debug, Eq, PartialEq)]
121 struct Flag {
122 number: i8,
123 points: HashSet<Point>
124 }
125
126 impl Flag {
127 pub const fn new(number: i8, points: HashSet<Point>) -> Self {
128 Self { number, points }
129 }
130
131 pub fn contains(&self, other: &Self) -> bool {
132 self.number >= other.number
133 && self.points.is_superset(&other.points)
134 }
135 }
136
137 impl PartialOrd for Flag {
138 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
139 if self == other {
140 return Some(Ordering::Equal)
141 }
142
143 if self.contains(other) {
144 return Some(Ordering::Greater)
145 }
146
147 if other.contains(self) {
148 return Some(Ordering::Less)
149 }
150
151 None
152 }
153 }
154
155 impl Sub for &Flag {
156 type Output = Flag;
157
158 fn sub(self, rhs: Self) -> Self::Output {
159 let mut points = self.points.clone();
164
165 for point in &rhs.points {
166 points.remove(point);
167 }
168
169 Flag::new(self.number - rhs.number, points)
170 }
171 }
172
173 impl Hash for Flag {
174 fn hash<H: Hasher>(&self, state: &mut H) {
175 self.number.hash(state);
176 for point in &self.points {
177 point.hash(state)
178 }
179 }
180 }
181
182 #[cfg(feature = "linked-hash-set")]
183 let mut flags = hashlink::LinkedHashSet::new();
184 #[cfg(not(feature = "linked-hash-set"))]
185 let mut flags = HashSet::new();
186
187 for point in size.points() {
188 let CellType::Safe(mut required) = state.board[point].cell_type else {
189 continue
190 };
191
192 for point in size.neighbours(point) {
193 if state.board[point].cell_state == CellState::Flagged {
194 required = required.saturating_sub(1)
195 }
196 }
197
198 if required == 0 {
199 continue
200 }
201
202 let neighbours: HashSet<_> = size.neighbours(point)
203 .filter(|e| state.board[*e].cell_state == CellState::Unknown)
204 .collect();
205
206 if neighbours.is_empty() {
207 continue
208 }
209
210 flags.insert(Flag::new(required as i8, neighbours));
211 }
212
213 let mut changed = true;
214 while changed {
215
216 let mut to_add = HashSet::new();
217 for flag in &flags {
218 {
220 let contained_flags: Vec<_> = flags.iter()
221 .filter(|e| flag >= e)
222 .collect();
223
224 for contained in contained_flags {
225 let remaining = flag - contained;
226
227 if remaining.points.is_empty() {
228 continue
229 }
230
231 if remaining.number == 0 {
232 return Some((Move::multi(
233 remaining.points
234 .into_iter()
235 .map(|e| Action::new(e, Reveal))
236 .collect(),
237 Some(Reason::new(MiaLogic::RegionDeductionReveal, contained.points.clone()))
238 ), MiaLogic::RegionDeductionReveal))
239 } else if remaining.number > 0 && remaining.number as usize == remaining.points.len() {
240 return Some((Move::multi(
241 remaining.points
242 .into_iter()
243 .map(|e| Action::new(e, Flag))
244 .collect(),
245 Some(Reason::new(MiaLogic::RegionDeductionFlag, contained.points.clone()))
246 ), MiaLogic::RegionDeductionFlag))
247
248 }
249
250 to_add.insert(remaining);
251 }
252 }
253
254 {
256 let touching_flags = flags.iter()
257 .filter(|e| e.points.iter()
258 .any(|e| flag.points.contains(e)));
259
260 for touching in touching_flags {
261 let remaining = flag - touching;
262
263 if remaining.points.is_empty() {
264 continue
265 }
266
267 if remaining.number > 0 && remaining.number as usize == remaining.points.len() {
268 return Some((Move::multi(
269 remaining.points
270 .into_iter()
271 .map(|e| Action::new(e, Flag))
272 .collect(),
273 Some(Reason::new(MiaLogic::RegionDeductionFlag, touching.points.clone()))
274 ), MiaLogic::RegionDeductionFlag))
275 }
276 }
277 }
278 }
279
280 changed = to_add.into_iter()
281 .map(|e| flags.insert(e))
282 .reduce(|a, b| a || b)
283 .unwrap_or(false);
284 }
285
286 if state.remaining_mines == 0 {
287 let clicks: HashSet<_> = size.points()
288 .filter(|e| state.board[*e].cell_state == CellState::Unknown)
289 .map(|e| Action::new(e, Reveal))
290 .collect();
291
292 if !clicks.is_empty() {
293 return Some((Move::multi(clicks, Some(Reason::new(MiaLogic::ZeroMinesRemaining, HashSet::new()))), MiaLogic::ZeroMinesRemaining))
294 }
295 }
296
297 if self.skill_level < Level::Expert {
298 return None
299 }
300
301 let mut empties = HashSet::new();
302 let mut adjacents = HashSet::new();
303
304 for point in size.points() {
305 if state.board[point].cell_state == CellState::Unknown {
306 for neighbour in size.neighbours(point) {
307 if matches!(state.board[neighbour].cell_type, CellType::Safe(number) if number > 0) {
308 empties.insert(point);
309 adjacents.insert(neighbour);
310 }
311 }
312 }
313 }
314
315 if empties.len() < Self::BRUTE_FORCE_LIMIT && !adjacents.is_empty() {
316 let mut result = BruteForceResult::new(size);
320
321 brute_force_3(&empties, &adjacents.iter().copied().collect(), 0, &mut state.clone(), &mut result);
322 if result.solve {
325 let mut clicks = HashSet::new();
326
327 for point in empties.iter().copied() {
328 if result.never_flag.contains(&point) {
329 clicks.insert(Action::new(point, Reveal));
330 }
331 if result.always_flag.contains(&point) {
332 clicks.insert(Action::new(point, Flag));
333 }
334 }
335
336 if !clicks.is_empty() {
337 return Some((Move::multi(clicks, Some(Reason::new(MiaLogic::BruteForce, empties))), MiaLogic::BruteForce))
338 }
339
340 if result.exhaustion {
341 for point in size.points() {
342 if state.board[point].cell_state == CellState::Unknown
343 && result.never_flag.contains(&point) {
344 clicks.insert(Action::new(point, Reveal));
345 }
346 }
347 }
348
349 if !clicks.is_empty() {
350 return Some((Move::multi(clicks, Some(Reason::new(MiaLogic::BruteForceExhaustion, empties))), MiaLogic::BruteForceExhaustion))
351 }
352
353 }
354
355 }
356
357 None
358 }
359}
360
361impl Solver for MiaSolver {
362
363 fn solve(&self, state: &GameState) -> Option<Move> {
364 self.internal_solve(state)
365 .map(|(e, _)| e)
366 }
367
368 fn solve_game(&self, minsweeper: &mut dyn Minsweeper) -> GameResult {
369 let mut requirement_met = self.required_level.is_none();
370 let required_logic = self.required_level
371 .map(Level::logics)
372 .unwrap_or_default();
373 let mut state = minsweeper.gamestate();
374
375 while state.status == GameStatus::Playing {
376 let Some((Move { actions, ..}, logic)) = self.internal_solve(state) else { break };
377
378 if !requirement_met && required_logic.contains(logic) {
379 requirement_met = true;
380 }
381
382 for action in actions {
383 state = minsweeper.action(action).into()
384 }
385 }
386
387 match state.status {
388 GameStatus::Won if requirement_met => GameResult::Won,
389 GameStatus::Lost => GameResult::Lost,
390 GameStatus::Playing => GameResult::Resigned,
391 _ => GameResult::Resigned
392 }
393
394 }
395}
396
397struct BruteForceResult {
398 solve: bool,
399 always_flag: HashSet<Point>,
400 never_flag: HashSet<Point>,
401 exhaustion: bool
402}
403
404impl BruteForceResult {
405 pub fn new(board_size: BoardSize) -> Self {
406 Self { solve: false, always_flag: board_size.points().collect(), never_flag: board_size.points().collect(), exhaustion: true }
407 }
408}
409
410fn brute_force_3(empties: &HashSet<Point>, adjacents: &Vec<Point>, adjacent_index: usize, state: &mut GameState, result: &mut BruteForceResult) {
411 if is_solved(&state.board) {
412 if state.remaining_mines != 0 {
413 result.exhaustion = false;
414 }
415
416 for point in empties {
417 if state.board[*point].cell_state == CellState::Flagged {
418 result.never_flag.remove(point);
419 } else {
420 result.always_flag.remove(point);
421 }
422 }
423
424 result.solve = true;
425 return
426 }
427 if state.remaining_mines == 0 {
428 return
429 }
430 if adjacent_index >= adjacents.len() {
431 return
432 }
433
434 let adjacent = adjacents[adjacent_index];
435 let CellType::Safe(n) = state.board[adjacent].cell_type else { unreachable!() };
436 let flagged = state.board.size()
437 .neighbours(adjacent)
438 .filter(|point| state.board[*point].cell_state == CellState::Flagged)
439 .count();
440 if n < flagged as u8 {
441 return
442 }
443 let flaggable = state.board.size()
444 .neighbours(adjacent)
445 .filter(|point| state.board[*point].cell_state == CellState::Unknown)
446 .collect();
447 recurse_flag(empties, adjacents, adjacent_index, &flaggable, 0, n as usize - flagged, state, result);
448}
449
450fn recurse_flag(empties: &HashSet<Point>, adjacents: &Vec<Point>, adjacent_index: usize, flaggable: &Vec<Point>, start: usize, mines_to_flag: usize, state: &mut GameState, result: &mut BruteForceResult) {
451
452 if mines_to_flag == 0 {
453 for point in &flaggable[start..] {
454 simulate_reveal(state, *point);
455 }
456 brute_force_3(empties, adjacents, adjacent_index + 1, state, result);
457 for point in &flaggable[start..] {
458 simulate_unreveal(state, *point);
459 }
460 return
461 }
462 for i in start..flaggable.len() {
463
464 simulate_right_click(state, flaggable[i]);
465 recurse_flag(empties, adjacents, adjacent_index, flaggable, i + 1, mines_to_flag - 1, state, result);
466 simulate_right_click(state, flaggable[i]);
467 simulate_reveal(state, flaggable[i])
468 }
469 for point in &flaggable[start..] {
470 simulate_unreveal(state, *point);
471 }
472}
473
474fn is_solved(board: &Board) -> bool {
531 for point in board.size().points() {
532 if let CellType::Safe(n) = board[point].cell_type && n > 0
533 && board.size()
534 .neighbours(point)
535 .map(|neighbour| if board[neighbour].cell_state == CellState::Flagged { 1 } else { 0 })
536 .sum::<u8>() != n {
537 return false
538 }
539 }
540 true
541}
542
543fn simulate_right_click(state: &mut GameState, point: Point) {
630 let cell = &mut state.board[point];
631 match cell.cell_state {
632 CellState::Unknown => {
633 cell.cell_state = CellState::Flagged;
634 state.remaining_mines -= 1;
635 }
636 CellState::Flagged => {
637 cell.cell_state = CellState::Unknown;
638 state.remaining_mines += 1;
639 }
640 CellState::Revealed => unreachable!()
641 }
642}
643
644fn simulate_reveal(state: &mut GameState, point: Point) {
645 state.board[point].cell_state = CellState::Revealed;
648}
649fn simulate_unreveal(state: &mut GameState, point: Point) {
650 state.board[point].cell_state = CellState::Unknown;
653}
654
655
656#[derive(EnumSetType, Debug)]
657pub enum MiaLogic {
658 Chord,
659 FlagChord,
660 RegionDeductionReveal,
661 RegionDeductionFlag,
662 ZeroMinesRemaining,
663 BruteForce,
664 BruteForceExhaustion,
665}
666
667impl Display for MiaLogic {
668 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
669 match self {
670 MiaLogic::Chord => write!(f, "the amount of flags around the cell matches its number"),
671 MiaLogic::FlagChord => write!(f, "the amount of flaggable cells around the cell matches its number"),
672 MiaLogic::RegionDeductionReveal => write!(f, "the surrounding cells force the cells to be safe"),
673 MiaLogic::RegionDeductionFlag => write!(f, "the surrounding cells force the cells to be a mine"),
674 MiaLogic::ZeroMinesRemaining => write!(f, "0 mines remaining, all unknown cells must be safe"),
675 MiaLogic::BruteForce => write!(f, "in every possible mine configuration the cells are safe/mines"),
676 MiaLogic::BruteForceExhaustion => write!(f, "in every possible mine configuration every mine is determined, all unused cells must be safe")
677 }
678 }
679}
680
681impl Logic for MiaLogic {
682
683}