1use crate::{MnkBoard, PlaceError, Player};
2use std::error::Error;
3use std::fmt;
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7#[non_exhaustive]
8pub enum PlayError {
9 GameOver(GameStatus),
11 PlaceError(PlaceError),
13 RuleError {
15 message: String,
17 },
18}
19
20impl fmt::Display for PlayError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::GameOver(status) => write!(f, "game already over: {status}"),
24 Self::PlaceError(place_error) => write!(f, "impossible move: {place_error}"),
25 Self::RuleError { message } => write!(f, "illegal move: {message}"),
26 }
27 }
28}
29
30impl Error for PlayError {}
31
32#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum GameStatus {
35 Drawn,
37 Ongoing {
39 next: Player,
41 },
42 Won(Player),
44}
45
46impl fmt::Display for GameStatus {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Drawn => write!(f, "Draw"),
50 Self::Ongoing { next } => write!(f, "Next: {next}"),
51 Self::Won(player) => write!(f, "{player} won!"),
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
66pub struct MnkGame<const R: usize, const C: usize, const K: usize> {
67 board: MnkBoard<R, C, K>,
68 status: GameStatus,
69}
70
71impl<const R: usize, const C: usize, const K: usize> MnkGame<R, C, K> {
72 #[must_use]
75 pub const fn new() -> Self {
76 Self {
77 board: MnkBoard::<R, C, K>::new(),
78 status: GameStatus::Ongoing { next: Player::X },
79 }
80 }
81
82 #[must_use]
84 pub const fn board(&self) -> &MnkBoard<R, C, K> {
85 &self.board
86 }
87
88 #[must_use]
90 pub const fn status(&self) -> GameStatus {
91 self.status
92 }
93
94 pub fn play_at(&mut self, row: usize, column: usize) -> Result<(), PlayError> {
105 match self.status {
106 GameStatus::Drawn | GameStatus::Won(_) => Err(PlayError::GameOver(self.status)),
107 GameStatus::Ongoing { next } => self.board.place(next, row, column).map_or_else(
108 |err| Err(PlayError::PlaceError(err)),
109 |()| {
110 self.status = GameStatus::Ongoing { next: !next };
111 self.update_status();
112 Ok(())
113 },
114 ),
115 }
116 }
117
118 fn update_status(&mut self) {
123 self.status = self.board.winner().map_or_else(
124 || {
125 if self.board.full() {
126 GameStatus::Drawn
127 } else {
128 self.status }
130 },
131 GameStatus::Won,
132 );
133 }
134}
135
136impl<const R: usize, const C: usize, const K: usize> Default for MnkGame<R, C, K> {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl<const R: usize, const C: usize, const K: usize> From<MnkGame<R, C, K>> for MnkBoard<R, C, K> {
143 fn from(game: MnkGame<R, C, K>) -> Self {
144 game.board
145 }
146}
147
148impl<const R: usize, const C: usize, const K: usize> fmt::Display for MnkGame<R, C, K> {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
150 write!(f, "{}\n{}", self.board, self.status)
151 }
152}
153
154#[cfg(test)]
155mod test_play_at {
156 use super::*;
157 use crate::Space;
158
159 #[test]
160 fn rejects_finished_games() {
161 let mut drawn = MnkGame::<1, 1, 1>::new();
162 drawn.status = GameStatus::Drawn;
163 assert_eq!(
164 drawn.play_at(0, 0),
165 Err(PlayError::GameOver(GameStatus::Drawn))
166 );
167 assert_eq!(drawn.board, MnkBoard::<1, 1, 1>::new());
168
169 let mut x_won = MnkGame::<1, 1, 1>::new();
170 x_won.status = GameStatus::Won(Player::X);
171 assert_eq!(
172 x_won.play_at(0, 0),
173 Err(PlayError::GameOver(GameStatus::Won(Player::X)))
174 );
175 assert_eq!(x_won.board, MnkBoard::<1, 1, 1>::new());
176
177 let mut o_won = MnkGame::<1, 1, 1>::new();
178 o_won.status = GameStatus::Won(Player::O);
179 assert_eq!(
180 o_won.play_at(0, 0),
181 Err(PlayError::GameOver(GameStatus::Won(Player::O)))
182 );
183 assert_eq!(o_won.board, MnkBoard::<1, 1, 1>::new());
184 }
185
186 #[test]
187 fn rejects_place_errors() {
188 let mut empty = MnkGame::<1, 1, 1>::new();
189 assert_eq!(
190 empty.play_at(1, 0),
191 Err(PlayError::PlaceError(PlaceError::OutOfBounds {
192 row: 1,
193 column: 0
194 }))
195 );
196 }
197
198 #[test]
199 fn depends_on_next_player() {
200 let mut x_plays = MnkGame::<1, 1, 1>::new();
201 assert_eq!(x_plays.play_at(0, 0), Ok(()));
202 assert_eq!(x_plays.board.get(0, 0), Some(&Space::Stone(Player::X)));
203
204 let mut o_plays = MnkGame::<1, 1, 1>::new();
205 o_plays.status = GameStatus::Ongoing { next: Player::O };
206 assert_eq!(o_plays.play_at(0, 0), Ok(()));
207 assert_eq!(o_plays.board.get(0, 0), Some(&Space::Stone(Player::O)));
208 }
209
210 #[test]
211 fn swaps_next_player() {
212 let mut x_plays = MnkGame::<2, 2, 2>::new();
213 assert_eq!(x_plays.play_at(0, 0), Ok(()));
214 assert_eq!(x_plays.status, GameStatus::Ongoing { next: Player::O });
215
216 let mut o_plays = MnkGame::<2, 2, 2>::new();
217 o_plays.status = GameStatus::Ongoing { next: Player::O };
218 assert_eq!(o_plays.play_at(0, 0), Ok(()));
219 assert_eq!(o_plays.status, GameStatus::Ongoing { next: Player::X });
220 }
221
222 #[test]
223 fn updates_status() {
224 let mut x_wins = MnkGame::<1, 1, 1>::new();
225 assert_eq!(x_wins.play_at(0, 0), Ok(()));
226 assert_eq!(x_wins.status, GameStatus::Won(Player::X));
227 }
228}
229
230#[cfg(test)]
231mod test_update_status {
232 use super::*;
233 use crate::Space;
234
235 #[test]
236 fn detects_wins() {
237 let mut x_wins = MnkGame::<1, 1, 1>::new();
238 x_wins.board = MnkBoard::from([[Space::Stone(Player::X)]]);
239 x_wins.update_status();
240 assert_eq!(x_wins.status, GameStatus::Won(Player::X));
241
242 let mut o_wins = MnkGame::<1, 1, 1>::new();
243 o_wins.board = MnkBoard::from([[Space::Stone(Player::O)]]);
244 o_wins.update_status();
245 assert_eq!(o_wins.status, GameStatus::Won(Player::O));
246 }
247
248 #[test]
249 fn detects_draws() {
250 let mut drawn = MnkGame::<1, 1, 2>::new();
251 drawn.board = MnkBoard::from([[Space::Stone(Player::X)]]);
252 drawn.update_status();
253 assert_eq!(drawn.status, GameStatus::Drawn);
254 }
255
256 #[test]
257 fn detects_ongoing() {
258 let mut ongoing = MnkGame::<1, 1, 1>::new();
259 ongoing.update_status();
260 assert_eq!(ongoing.status, GameStatus::Ongoing { next: Player::X });
261 }
262}
263
264#[cfg(test)]
265mod test_mnk_game_display {
266 use crate::{GameStatus, MnkGame, Player};
267
268 #[test]
269 fn draw() {
270 let mut draw = MnkGame::<1, 1, 1>::new();
271 draw.status = GameStatus::Drawn;
272 assert_eq!(
273 draw.to_string(),
274 "+-+\n\
275 | |\n\
276 +-+\n\
277 Draw"
278 );
279 }
280
281 #[test]
282 fn ongoing() {
283 let x_next = MnkGame::<1, 1, 1>::new();
284 assert_eq!(
285 x_next.to_string(),
286 "+-+\n\
287 | |\n\
288 +-+\n\
289 Next: X"
290 );
291
292 let mut o_next = MnkGame::<1, 1, 1>::new();
293 o_next.status = GameStatus::Ongoing { next: Player::O };
294 assert_eq!(
295 o_next.to_string(),
296 "+-+\n\
297 | |\n\
298 +-+\n\
299 Next: O"
300 );
301 }
302
303 #[test]
304 fn won() {
305 let mut x_won = MnkGame::<1, 1, 1>::new();
306 x_won.status = GameStatus::Won(Player::X);
307 assert_eq!(
308 x_won.to_string(),
309 "+-+\n\
310 | |\n\
311 +-+\n\
312 X won!"
313 );
314
315 let mut o_won = MnkGame::<1, 1, 1>::new();
316 o_won.status = GameStatus::Won(Player::O);
317 assert_eq!(
318 o_won.to_string(),
319 "+-+\n\
320 | |\n\
321 +-+\n\
322 O won!"
323 );
324 }
325}