1use anyhow::{Context, Result, bail, ensure};
4use rand::seq::SliceRandom;
5use serde::Serialize;
6use std::ops::RangeInclusive;
7
8pub const W_MAX: i32 = 30;
9pub const H_MAX: i32 = 16;
10const W_RANGE: RangeInclusive<i32> = 1..=W_MAX;
11const H_RANGE: RangeInclusive<i32> = 1..=W_MAX;
12
13const DXY: &[(i32, i32)] = &[
14 (-1, -1),
15 (-1, 0),
16 (-1, 1),
17 (0, -1),
18 (0, 1),
19 (1, -1),
20 (1, 0),
21 (1, 1),
22];
23
24#[derive(Debug, Clone, Copy, Default, Serialize)]
25pub enum GameState {
26 #[default]
27 Initialized,
28 Playing,
29 Cleared,
30 Failed,
31}
32
33#[derive(Debug, Clone, Copy)]
34pub enum Cell {
35 Mine,
36 Number(u8),
37}
38
39#[derive(Debug, Clone, Copy)]
40pub struct Config {
41 pub width: i32,
42 pub height: i32,
43 pub mine_count: i32,
44}
45
46pub enum Level {
47 Easy,
48 Normal,
49 Hard,
50}
51
52#[derive(Debug, Clone, Serialize)]
53struct MsStateJson {
54 state: GameState,
55 width: i32,
56 height: i32,
57 mine_count: i32,
58 board: Vec<String>,
59}
60
61impl Level {
62 pub fn to_config(&self) -> Config {
63 match self {
64 Self::Easy => Config {
65 width: 9,
66 height: 9,
67 mine_count: 10,
68 },
69 Self::Normal => Config {
70 width: 16,
71 height: 16,
72 mine_count: 40,
73 },
74 Self::Hard => Config {
75 width: 30,
76 height: 16,
77 mine_count: 99,
78 },
79 }
80 }
81}
82
83pub struct MineSweeper {
84 pub state: GameState,
85 pub width: i32,
86 pub height: i32,
87 pub mine_count: i32,
88 board: Vec<Cell>,
89 revealed: Vec<bool>,
90}
91
92impl MineSweeper {
93 pub fn new(config: Config) -> Result<Self> {
94 let width = config.width;
95 let height = config.height;
96 ensure!(W_RANGE.contains(&width));
97 ensure!(H_RANGE.contains(&height));
98 let size = width * height;
100 let mine_count = config.mine_count;
101 ensure!((1..size).contains(&mine_count));
102
103 let mut rng = rand::rng();
105 let mut board: Vec<_> = std::iter::repeat_n(Cell::Mine, mine_count as usize)
106 .chain(std::iter::repeat_n(
107 Cell::Number(0),
108 (size - mine_count) as usize,
109 ))
110 .collect();
111 board.shuffle(&mut rng);
112
113 for y in 0..height {
115 for x in 0..width {
116 let idx = Self::convert_raw(x, y, width, height).unwrap();
117 if matches!(board[idx], Cell::Mine) {
118 continue;
119 }
120 let mut count = 0;
121 for (dx, dy) in DXY {
122 let nx = x + dx;
123 let ny = y + dy;
124 if let Some(nidx) = Self::convert_raw(nx, ny, width, height)
125 && matches!(board[nidx], Cell::Mine)
126 {
127 count += 1;
128 }
129 }
130 board[idx] = Cell::Number(count);
131 }
132 }
133
134 let obj = Self {
135 state: GameState::Initialized,
136 width,
137 height,
138 mine_count,
139 board,
140 revealed: vec![false; (config.width * config.height) as usize],
141 };
142
143 Ok(obj)
144 }
145
146 pub fn to_json(&self) -> String {
147 serde_json::to_string(&self.to_json_raw()).unwrap()
148 }
149
150 pub fn to_json_pretty(&self) -> String {
151 serde_json::to_string_pretty(&self.to_json_raw()).unwrap()
152 }
153
154 fn to_json_raw(&self) -> MsStateJson {
155 let mut board = Vec::with_capacity(self.height as usize);
156 for y in 0..self.height {
157 let mut line = String::with_capacity(self.width as usize);
158 for x in 0..self.width {
159 let idx = self.convert(x, y).unwrap();
160 let c = if self.revealed[idx] {
161 match self.board[idx] {
162 Cell::Mine => '*',
163 Cell::Number(n) => (b'0' + n) as char,
164 }
165 } else {
166 '.'
167 };
168 line.push(c);
169 }
170 board.push(line);
171 }
172
173 MsStateJson {
174 state: self.state,
175 width: self.width,
176 height: self.height,
177 mine_count: self.mine_count,
178 board,
179 }
180 }
181
182 pub fn reveal(&mut self, x: i32, y: i32) -> Result<GameState> {
183 if !(matches!(self.state, GameState::Initialized | GameState::Playing)) {
184 bail!("Already finished");
185 }
186
187 let _ = self.convert(x, y).context("Invalid x y")?;
188
189 self.reveal_raw(x, y).context("Already revealed")?;
190 self.state = self.check_state();
191
192 Ok(self.state)
193 }
194
195 fn reveal_raw(&mut self, x: i32, y: i32) -> Option<Cell> {
196 let idx = self.convert(x, y)?;
197 if !self.revealed[idx] {
198 self.revealed[idx] = true;
199 if let Cell::Number(n) = self.board[idx]
200 && n == 0
201 {
202 for (dy, dx) in DXY {
203 let nx = x + dx;
204 let ny = y + dy;
205 self.reveal_raw(nx, ny);
206 }
207 }
208 Some(self.board[idx])
209 } else {
210 None
211 }
212 }
213
214 fn check_state(&self) -> GameState {
215 debug_assert_eq!(self.board.len(), self.revealed.len());
216
217 let mut not_cleared = false;
218 for (cell, revealed) in self.board.iter().zip(self.revealed.iter()) {
219 if *revealed {
220 if matches!(cell, Cell::Mine) {
221 return GameState::Failed;
222 }
223 } else if !matches!(cell, Cell::Mine) {
224 not_cleared = true;
225 }
226 }
227
228 if not_cleared {
229 GameState::Playing
230 } else {
231 GameState::Cleared
232 }
233 }
234
235 fn convert(&self, x: i32, y: i32) -> Option<usize> {
236 Self::convert_raw(x, y, self.width, self.height)
237 }
238
239 fn convert_raw(x: i32, y: i32, w: i32, h: i32) -> Option<usize> {
240 if (0..w).contains(&x) && (0..h).contains(&y) {
241 Some((y * w + x) as usize)
242 } else {
243 None
244 }
245 }
246}
247
248#[cfg(test)]
249mod test {
250 use super::*;
251
252 #[test]
253 #[ignore]
254 fn ms_play() -> Result<()> {
256 let mut ms = MineSweeper::new(Level::Easy.to_config()).unwrap();
257 loop {
258 println!("{}", ms.to_json_pretty());
259
260 let mut input = String::new();
261 println!("Enter x y (or 'exit'):");
262 std::io::stdin().read_line(&mut input).unwrap();
263 let input = input.trim();
264 if input == "exit" {
265 break Ok(());
266 }
267
268 let mut iter = input.split_whitespace();
269 let x: i32 = iter.next().unwrap().parse().unwrap();
270 let y: i32 = iter.next().unwrap().parse().unwrap();
271 match ms.reveal(x, y) {
272 Ok(state) => match state {
273 GameState::Playing => println!("Playing"),
274 GameState::Cleared => {
275 println!("{}", ms.to_json_pretty());
276 println!("Cleared");
277 break Ok(());
278 }
279 GameState::Failed => {
280 println!("{}", ms.to_json_pretty());
281 println!("Failed");
282 break Ok(());
283 }
284 _ => {}
285 },
286 Err(e) => println!("Error: {e}"),
287 }
288 }
289 }
290
291 #[test]
292 fn ms_init() {
293 for _ in 0..100 {
294 let _ms = MineSweeper::new(Level::Easy.to_config()).unwrap();
295 let _ms = MineSweeper::new(Level::Normal.to_config()).unwrap();
296 let _ms = MineSweeper::new(Level::Hard.to_config()).unwrap();
297 }
298 }
299}