Skip to main content

mnk_games/
games.rs

1use crate::{MnkBoard, PlaceError, Player, Space};
2use std::error::Error;
3use std::fmt;
4
5/// Errors which may occur when playing a move.
6#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7#[non_exhaustive]
8pub enum PlayError {
9    /// An error which may occur when the game is already over.
10    GameOver(GameStatus),
11    /// An error which may occur when a stone cannot be placed at the indicated position.
12    PlaceError(PlaceError),
13    /// An error which may occur when a move is against a game's rules.
14    RuleError {
15        /// An informative message about the violated rule.
16        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/// The current status of a game.
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum GameStatus {
35    /// The game is over and is a draw.
36    Drawn,
37    /// The game is not over.
38    Ongoing {
39        /// The [`Player`] who will play the next move.
40        next: Player,
41    },
42    /// The game is over and has been won by the indicated [`Player`].
43    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/// Returns the first [`Player`] to have `win_length` consecutive [`Space::Stone`]s in the
57/// [`Iterator`].
58#[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/// Returns the first [`Player`] to be a winner in any of the passed runs.
87#[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/// A standard [*m,n,k*-game].
97///
98/// [`Player::X`] and [`Player::O`] alternate placing stones, in that order, on a board with `R`
99/// rows and `C` columns until one wins by having `K` stones in a row, column, or diagonal.
100///
101/// Methods are zero-indexed. Rows at least `R` and columns at least `C` are considered out of
102/// bounds.
103///
104/// [*m,n,k*-game]: https://en.wikipedia.org/wiki/M,n,k-game
105#[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    /// Returns a [`GameStatus::Ongoing`] `MnkGame<R, C, K>` with an empty board and current player
113    /// [`Player::X`].
114    #[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    /// The current state of the game's [`MnkBoard`].
123    #[must_use]
124    pub const fn board(&self) -> &MnkBoard<R, C> {
125        &self.board
126    }
127
128    /// The current [`GameStatus`] of the game.
129    #[must_use]
130    pub const fn status(&self) -> GameStatus {
131        self.status
132    }
133
134    /// Attempts to play at the indicated location.
135    ///
136    /// If successful, plays a stone at the indicated location, switches the current [`Player`],
137    /// and checks whether the [`GameStatus`] has changed. Never plays a stone if it also returns an
138    /// error.
139    ///
140    /// # Errors
141    ///
142    /// - [`PlayError::GameOver`] if the game is [`GameStatus::Drawn`] or [`GameStatus::Won`].
143    /// - [`PlayError::PlaceError`] if the indicated location is not a valid move.
144    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    /// Changes the `status` field.
159    ///
160    /// [`GameStatus::Won`] if the game has been won. Otherwise, [`GameStatus::Drawn`] if the board
161    /// is full and [`GameStatus::Ongoing`] otherwise.
162    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 // To retain the wrapped Player
169                }
170            },
171            GameStatus::Won,
172        );
173    }
174
175    /// Returns the winner of the game, or [`None`] if neither [`Player`] has won.
176    #[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}