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 #[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 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 {
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 {
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 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}