Skip to main content

mnk_games/
games.rs

1use crate::{MnkBoard, PlaceError, Player};
2use std::error::Error;
3use std::fmt;
4
5/// Errors which may occur when playing a move in an *m,n,k*-game.
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/// A standard [*m,n,k*-game].
57///
58/// [`Player::X`] and [`Player::O`] alternate placing stones, in that order, on a board with `R`
59/// rows and `C` columns until one wins by having `K` stones in a row, column, or diagonal.
60///
61/// Methods are zero-indexed. Rows at least `R` and columns at least `C` are considered out of
62/// bounds.
63///
64/// [*m,n,k*-game]: https://en.wikipedia.org/wiki/M,n,k-game
65#[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    /// Returns a [`GameStatus::Ongoing`] `MnkGame<R, C, K>` with an empty board and current player
73    /// [`Player::X`].
74    #[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    /// The current state of the game's [`MnkBoard`].
83    #[must_use]
84    pub const fn board(&self) -> &MnkBoard<R, C, K> {
85        &self.board
86    }
87
88    /// The current [`GameStatus`] of the game.
89    #[must_use]
90    pub const fn status(&self) -> GameStatus {
91        self.status
92    }
93
94    /// Attempts to play at the indicated location.
95    ///
96    /// If successful, plays a stone at the indicated location, switches the current [`Player`],
97    /// and checks whether the [`GameStatus`] has changed. Never plays a stone if it also returns an
98    /// error.
99    ///
100    /// # Errors
101    ///
102    /// - [`PlayError::GameOver`] if the game is [`GameStatus::Drawn`] or [`GameStatus::Won`].
103    /// - [`PlayError::PlaceError`] if the indicated location is not a valid move.
104    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    /// Changes the `status` field.
119    ///
120    /// [`GameStatus::Won`] if the game has been won. Otherwise, [`GameStatus::Drawn`] if the board
121    /// is full and [`GameStatus::Ongoing`] otherwise.
122    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 // To retain the wrapped Player
129                }
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}