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;
10use hashlink::LinkedHashSet;
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 #[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 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 {
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 {
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 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}