1use crate::{MnkBoard, PlaceError, Player, Space};
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#[must_use]
59fn winner_in_run<'a>(
60 run: impl IntoIterator<Item = &'a Space>,
61 win_length: usize,
62) -> Option<Player> {
63 let mut consecutive = 0;
64 let mut previous = &Space::Empty;
65 for space in run {
66 match *space {
67 Space::Empty => {
68 consecutive = 0;
69 }
70 Space::Stone(player) => {
71 if space == previous {
72 consecutive += 1;
73 } else {
74 consecutive = 1;
75 }
76 if consecutive == win_length {
77 return Some(player);
78 }
79 }
80 }
81 previous = space;
82 }
83 None
84}
85
86#[must_use]
88fn winner_in_runs<'a>(
89 runs: impl IntoIterator<Item = impl IntoIterator<Item = &'a Space>>,
90 win_length: usize,
91) -> Option<Player> {
92 let mut winners = runs.into_iter().map(|run| winner_in_run(run, win_length));
93 winners.find(Option::is_some).flatten()
94}
95
96#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
106pub struct MnkGame<const R: usize, const C: usize, const K: usize> {
107 board: MnkBoard<R, C>,
108 status: GameStatus,
109}
110
111impl<const R: usize, const C: usize, const K: usize> MnkGame<R, C, K> {
112 #[must_use]
115 pub const fn new() -> Self {
116 Self {
117 board: MnkBoard::<R, C>::new(),
118 status: GameStatus::Ongoing { next: Player::X },
119 }
120 }
121
122 #[must_use]
124 pub const fn board(&self) -> &MnkBoard<R, C> {
125 &self.board
126 }
127
128 #[must_use]
130 pub const fn status(&self) -> GameStatus {
131 self.status
132 }
133
134 pub fn play_at(&mut self, row: usize, column: usize) -> Result<(), PlayError> {
145 match self.status {
146 GameStatus::Drawn | GameStatus::Won(_) => Err(PlayError::GameOver(self.status)),
147 GameStatus::Ongoing { next } => self.board.place(next, row, column).map_or_else(
148 |err| Err(PlayError::PlaceError(err)),
149 |()| {
150 self.status = GameStatus::Ongoing { next: !next };
151 self.update_status();
152 Ok(())
153 },
154 ),
155 }
156 }
157
158 fn update_status(&mut self) {
163 self.status = self.winner().map_or_else(
164 || {
165 if self.board.full() {
166 GameStatus::Drawn
167 } else {
168 self.status }
170 },
171 GameStatus::Won,
172 );
173 }
174
175 #[must_use]
177 fn winner(&self) -> Option<Player> {
178 if C >= K {
179 let winner = winner_in_runs(self.board.rows(), K);
180 if winner.is_some() {
181 return winner;
182 }
183 }
184 if R >= K {
185 let winner = winner_in_runs(self.board.columns(), K);
186 if winner.is_some() {
187 return winner;
188 }
189 }
190 if R >= K && C >= K {
191 let mut winner = winner_in_runs(self.board.top_right_diagonals(K), K);
192 if winner.is_some() {
193 return winner;
194 }
195 winner = winner_in_runs(self.board.left_down_diagonals(K), K);
196 if winner.is_some() {
197 return winner;
198 }
199 winner = winner_in_runs(self.board.top_left_diagonals(K), K);
200 if winner.is_some() {
201 return winner;
202 }
203 winner_in_runs(self.board.right_down_diagonals(K), K)
204 } else {
205 None
206 }
207 }
208}
209
210impl<const R: usize, const C: usize, const K: usize> Default for MnkGame<R, C, K> {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl<const R: usize, const C: usize, const K: usize> From<MnkGame<R, C, K>> for MnkBoard<R, C> {
217 fn from(game: MnkGame<R, C, K>) -> Self {
218 game.board
219 }
220}
221
222impl<const R: usize, const C: usize, const K: usize> fmt::Display for MnkGame<R, C, K> {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
224 write!(f, "{}\n{}", self.board, self.status)
225 }
226}
227
228#[cfg(test)]
229mod test_winner_in_run {
230 use super::*;
231
232 #[test]
233 fn trivial() {
234 let empty: [&Space; 0] = [];
235 assert!(winner_in_run(empty, 1).is_none());
236 assert!(winner_in_run(empty, 2).is_none());
237 assert!(winner_in_run(empty, 3).is_none());
238
239 let one_empty = [&Space::Empty];
240 assert!(winner_in_run(one_empty, 1).is_none());
241 assert!(winner_in_run(one_empty, 2).is_none());
242
243 let one_x = [&Space::Stone(Player::X)];
244 assert_eq!(winner_in_run(one_x, 1), Some(Player::X));
245 assert!(winner_in_run(one_x, 2).is_none());
246
247 let one_o = [&Space::Stone(Player::O)];
248 assert_eq!(winner_in_run(one_o, 1), Some(Player::O));
249 assert!(winner_in_run(one_o, 2).is_none());
250 }
251
252 #[test]
253 fn single_player() {
254 let right_run = [
255 &Space::Empty,
256 &Space::Empty,
257 &Space::Stone(Player::X),
258 &Space::Stone(Player::X),
259 &Space::Stone(Player::X),
260 ];
261 assert_eq!(winner_in_run(right_run, 3), Some(Player::X));
262 assert!(winner_in_run(right_run, 4).is_none());
263
264 let interrupted = [
265 &Space::Stone(Player::X),
266 &Space::Stone(Player::X),
267 &Space::Empty,
268 &Space::Stone(Player::X),
269 &Space::Stone(Player::X),
270 ];
271 assert_eq!(winner_in_run(interrupted, 2), Some(Player::X));
272 assert!(winner_in_run(interrupted, 3).is_none());
273
274 let bookend = [
275 &Space::Empty,
276 &Space::Stone(Player::X),
277 &Space::Stone(Player::X),
278 &Space::Stone(Player::X),
279 &Space::Empty,
280 ];
281 assert_eq!(winner_in_run(bookend, 3), Some(Player::X));
282 assert!(winner_in_run(bookend, 4).is_none());
283 }
284
285 #[test]
286 fn two_player() {
287 let left_heavy = [
288 &Space::Stone(Player::X),
289 &Space::Stone(Player::X),
290 &Space::Stone(Player::O),
291 ];
292 assert_eq!(winner_in_run(left_heavy, 2), Some(Player::X));
293 assert!(winner_in_run(left_heavy, 3).is_none());
294
295 let right_heavy = [
296 &Space::Stone(Player::O),
297 &Space::Stone(Player::X),
298 &Space::Stone(Player::X),
299 ];
300 assert_eq!(winner_in_run(right_heavy, 2), Some(Player::X));
301 assert!(winner_in_run(right_heavy, 3).is_none());
302
303 let interrupted = [
304 &Space::Stone(Player::X),
305 &Space::Stone(Player::O),
306 &Space::Stone(Player::X),
307 ];
308 assert!(winner_in_run(interrupted, 2).is_none());
309 assert!(winner_in_run(interrupted, 3).is_none());
310 }
311}
312
313#[cfg(test)]
314mod test_winner_in_runs {
315 use super::*;
316 use std::iter;
317
318 #[test]
319 fn trivial() {
320 let empty: iter::Empty<iter::Empty<&Space>> = iter::empty();
321 assert!(winner_in_runs(empty, 1).is_none());
322
323 let single = iter::once(iter::once(&Space::Stone(Player::X)));
324 assert_eq!(winner_in_runs(single, 1), Some(Player::X));
325 }
326
327 #[test]
328 fn several_runs() {
329 let delayed = [
330 iter::once(&Space::Empty),
331 iter::once(&Space::Stone(Player::X)),
332 ];
333 assert_eq!(winner_in_runs(delayed, 1), Some(Player::X));
334
335 let all_empty = [
336 iter::once(&Space::Empty),
337 iter::once(&Space::Empty),
338 iter::once(&Space::Empty),
339 ];
340 assert!(winner_in_runs(all_empty, 1).is_none());
341 }
342}
343
344#[cfg(test)]
345mod test_play_at {
346 use super::*;
347 use crate::Space;
348
349 #[test]
350 fn rejects_finished_games() {
351 let mut drawn: MnkGame<1, 1, 1> = MnkGame::new();
352 drawn.status = GameStatus::Drawn;
353 assert_eq!(
354 drawn.play_at(0, 0),
355 Err(PlayError::GameOver(GameStatus::Drawn))
356 );
357 assert_eq!(drawn.board, MnkBoard::<1, 1>::new());
358
359 let mut x_won: MnkGame<1, 1, 1> = MnkGame::new();
360 x_won.status = GameStatus::Won(Player::X);
361 assert_eq!(
362 x_won.play_at(0, 0),
363 Err(PlayError::GameOver(GameStatus::Won(Player::X)))
364 );
365 assert_eq!(x_won.board, MnkBoard::<1, 1>::new());
366
367 let mut o_won: MnkGame<1, 1, 1> = MnkGame::new();
368 o_won.status = GameStatus::Won(Player::O);
369 assert_eq!(
370 o_won.play_at(0, 0),
371 Err(PlayError::GameOver(GameStatus::Won(Player::O)))
372 );
373 assert_eq!(o_won.board, MnkBoard::<1, 1>::new());
374 }
375
376 #[test]
377 fn rejects_place_errors() {
378 let mut empty: MnkGame<1, 1, 1> = MnkGame::new();
379 assert_eq!(
380 empty.play_at(1, 0),
381 Err(PlayError::PlaceError(PlaceError::OutOfBounds {
382 row: 1,
383 column: 0
384 }))
385 );
386 }
387
388 #[test]
389 fn depends_on_next_player() {
390 let mut x_plays: MnkGame<1, 1, 1> = MnkGame::new();
391 assert_eq!(x_plays.play_at(0, 0), Ok(()));
392 assert_eq!(x_plays.board.get(0, 0), Some(&Space::Stone(Player::X)));
393
394 let mut o_plays: MnkGame<1, 1, 1> = MnkGame::new();
395 o_plays.status = GameStatus::Ongoing { next: Player::O };
396 assert_eq!(o_plays.play_at(0, 0), Ok(()));
397 assert_eq!(o_plays.board.get(0, 0), Some(&Space::Stone(Player::O)));
398 }
399
400 #[test]
401 fn swaps_next_player() {
402 let mut x_plays: MnkGame<2, 2, 2> = MnkGame::new();
403 assert_eq!(x_plays.play_at(0, 0), Ok(()));
404 assert_eq!(x_plays.status, GameStatus::Ongoing { next: Player::O });
405
406 let mut o_plays: MnkGame<2, 2, 2> = MnkGame::new();
407 o_plays.status = GameStatus::Ongoing { next: Player::O };
408 assert_eq!(o_plays.play_at(0, 0), Ok(()));
409 assert_eq!(o_plays.status, GameStatus::Ongoing { next: Player::X });
410 }
411
412 #[test]
413 fn updates_status() {
414 let mut x_wins: MnkGame<1, 1, 1> = MnkGame::new();
415 assert_eq!(x_wins.play_at(0, 0), Ok(()));
416 assert_eq!(x_wins.status, GameStatus::Won(Player::X));
417 }
418}
419
420#[cfg(test)]
421mod test_update_status {
422 use super::*;
423 use crate::Space;
424
425 #[test]
426 fn detects_wins() {
427 let mut x_wins: MnkGame<1, 1, 1> = MnkGame::new();
428 x_wins.board = MnkBoard::from([[Space::Stone(Player::X)]]);
429 x_wins.update_status();
430 assert_eq!(x_wins.status, GameStatus::Won(Player::X));
431
432 let mut o_wins: MnkGame<1, 1, 1> = MnkGame::new();
433 o_wins.board = MnkBoard::from([[Space::Stone(Player::O)]]);
434 o_wins.update_status();
435 assert_eq!(o_wins.status, GameStatus::Won(Player::O));
436 }
437
438 #[test]
439 fn detects_draws() {
440 let mut drawn: MnkGame<1, 1, 2> = MnkGame::new();
441 drawn.board = MnkBoard::from([[Space::Stone(Player::X)]]);
442 drawn.update_status();
443 assert_eq!(drawn.status, GameStatus::Drawn);
444 }
445
446 #[test]
447 fn detects_ongoing() {
448 let mut ongoing: MnkGame<1, 1, 1> = MnkGame::new();
449 ongoing.update_status();
450 assert_eq!(ongoing.status, GameStatus::Ongoing { next: Player::X });
451 }
452}
453
454#[cfg(test)]
455mod test_winner {
456 use super::*;
457
458 fn ongoing_game<const R: usize, const C: usize, const K: usize>(
459 board: MnkBoard<R, C>,
460 ) -> MnkGame<R, C, K> {
461 MnkGame {
462 board,
463 status: GameStatus::Ongoing { next: Player::X },
464 }
465 }
466
467 #[test]
468 fn draws() {
469 let empty_0x0: MnkGame<0, 0, 1> = ongoing_game(MnkBoard::new());
470 assert!(empty_0x0.winner().is_none());
471
472 let empty_3x3: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::new());
473 assert!(empty_3x3.winner().is_none());
474
475 let drawn_3x3: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
476 [
477 Space::Stone(Player::X),
478 Space::Stone(Player::O),
479 Space::Stone(Player::X),
480 ],
481 [
482 Space::Stone(Player::X),
483 Space::Stone(Player::O),
484 Space::Stone(Player::O),
485 ],
486 [
487 Space::Stone(Player::O),
488 Space::Stone(Player::X),
489 Space::Stone(Player::X),
490 ],
491 ]));
492 assert!(drawn_3x3.winner().is_none());
493 }
494
495 #[test]
496 fn row_win() {
497 let row_win: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
498 [
499 Space::Stone(Player::X),
500 Space::Stone(Player::X),
501 Space::Stone(Player::X),
502 ],
503 [Space::Empty, Space::Empty, Space::Empty],
504 [Space::Empty, Space::Empty, Space::Empty],
505 ]));
506 assert_eq!(row_win.winner(), Some(Player::X));
507 }
508
509 #[test]
510 fn column_win() {
511 let column_win: MnkGame<3, 3, 3> = ongoing_game(MnkBoard::from([
512 [Space::Stone(Player::X), Space::Empty, Space::Empty],
513 [Space::Stone(Player::X), Space::Empty, Space::Empty],
514 [Space::Stone(Player::X), Space::Empty, Space::Empty],
515 ]));
516 assert_eq!(column_win.winner(), Some(Player::X));
517 }
518
519 #[test]
520 fn top_right_win() {
521 let top_right_win: MnkGame<3, 3, 2> = ongoing_game(MnkBoard::from([
522 [Space::Stone(Player::X), Space::Empty, Space::Empty],
523 [Space::Empty, Space::Stone(Player::X), Space::Empty],
524 [Space::Empty, Space::Empty, Space::Empty],
525 ]));
526 assert_eq!(top_right_win.winner(), Some(Player::X));
527 }
528
529 #[test]
530 fn left_down_win() {
531 let left_down_win: MnkGame<4, 3, 3> = ongoing_game(MnkBoard::from([
532 [Space::Empty, Space::Empty, Space::Empty],
533 [Space::Stone(Player::X), Space::Empty, Space::Empty],
534 [Space::Empty, Space::Stone(Player::X), Space::Empty],
535 [Space::Empty, Space::Empty, Space::Stone(Player::X)],
536 ]));
537 assert_eq!(left_down_win.winner(), Some(Player::X));
538 }
539
540 #[test]
541 fn top_left_win() {
542 let top_left_win: MnkGame<3, 3, 2> = ongoing_game(MnkBoard::from([
543 [Space::Empty, Space::Empty, Space::Stone(Player::X)],
544 [Space::Empty, Space::Stone(Player::X), Space::Empty],
545 [Space::Empty, Space::Empty, Space::Empty],
546 ]));
547 assert_eq!(top_left_win.winner(), Some(Player::X));
548 }
549
550 #[test]
551 fn right_down_win() {
552 let right_down_win: MnkGame<4, 3, 3> = ongoing_game(MnkBoard::from([
553 [Space::Empty, Space::Empty, Space::Empty],
554 [Space::Empty, Space::Empty, Space::Stone(Player::X)],
555 [Space::Empty, Space::Stone(Player::X), Space::Empty],
556 [Space::Stone(Player::X), Space::Empty, Space::Empty],
557 ]));
558 assert_eq!(right_down_win.winner(), Some(Player::X));
559 }
560}
561
562#[cfg(test)]
563mod test_mnk_game_display {
564 use crate::{GameStatus, MnkGame, Player};
565
566 #[test]
567 fn draw() {
568 let mut draw: MnkGame<1, 1, 1> = MnkGame::new();
569 draw.status = GameStatus::Drawn;
570 assert_eq!(
571 draw.to_string(),
572 "+-+\n\
573 | |\n\
574 +-+\n\
575 Draw"
576 );
577 }
578
579 #[test]
580 fn ongoing() {
581 let x_next: MnkGame<1, 1, 1> = MnkGame::new();
582 assert_eq!(
583 x_next.to_string(),
584 "+-+\n\
585 | |\n\
586 +-+\n\
587 Next: X"
588 );
589
590 let mut o_next: MnkGame<1, 1, 1> = MnkGame::new();
591 o_next.status = GameStatus::Ongoing { next: Player::O };
592 assert_eq!(
593 o_next.to_string(),
594 "+-+\n\
595 | |\n\
596 +-+\n\
597 Next: O"
598 );
599 }
600
601 #[test]
602 fn won() {
603 let mut x_won: MnkGame<1, 1, 1> = MnkGame::new();
604 x_won.status = GameStatus::Won(Player::X);
605 assert_eq!(
606 x_won.to_string(),
607 "+-+\n\
608 | |\n\
609 +-+\n\
610 X won!"
611 );
612
613 let mut o_won: MnkGame<1, 1, 1> = MnkGame::new();
614 o_won.status = GameStatus::Won(Player::O);
615 assert_eq!(
616 o_won.to_string(),
617 "+-+\n\
618 | |\n\
619 +-+\n\
620 O won!"
621 );
622 }
623}