super_ttt/
lib.rs

1//! # Super Tic Tic Toe
2//! This library provides a core that implements the logic
3//! for playing [Super Tic Tac Toe](https://en.wikipedia.org/wiki/Ultimate_tic-tac-toe).
4//!
5//! The rules of said game are explained in the Wikipedia entry, with **one exception**: since it wasn't specified in the Wikipedia article, the behavior for ties within small, traditional 3x3 tic tac toe boards will result in that board being unable to use. It will be "dead" or "locked"; nobody can use that board in their 3-in-a-row final win.
6//!
7//! ## Terminology
8//! Because "board of boards" can get confusing on what you're referring
9//! to, I'll define some terms:
10//!
11//! **Game:** the entire 9x9 super tic-tac-toe game or "large board"
12//!
13//! **Coordinates:** An (x, y) pair where x and y are integers between and including 0 to 2. It represents the location of a section (a square for a board, a board for a game).
14//!
15//! **Board:** a traditional 3x3 tic-tac-toe game or "small board"
16//!
17//! **Square:** a cell of a traditional tic-tac-toe board. It will either be empty or containing an `X`/`O`,
18//!
19//! **Square coordinates:** An (x, y) pair that, like a normal coordinate, represents the location of something. But unlike a regular coordinate, it represents the exact location of a specific square. X and Y will be integers between and including 0 to 8.
20#![warn(missing_docs)]
21
22use std::fmt::Display;
23pub mod errors;
24/// Represents a player (`X` or `O`)
25#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
26#[allow(missing_docs)]
27pub enum Player {
28    X,
29    O,
30}
31
32/// Represents a the content of a smaller Tic Tac Toe board
33#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
34#[allow(missing_docs)]
35pub enum Square {
36    #[default]
37    Empty,
38    Occupied(Player),
39}
40
41#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
42#[allow(missing_docs)]
43pub enum GameState {
44    Tie,
45    Winner(Player),
46    InProgress,
47}
48
49/// The size length of the board *and* the game. This should never change.
50pub const BOARD_SIZE: usize = 3;
51/// Represents the 3x3 traditional Tic Tac Toe board
52#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
53pub struct Board {
54    /// Self explanatory. Public to allow implementations of display methods
55    pub squares: [[Square; BOARD_SIZE]; BOARD_SIZE],
56}
57
58impl Board {
59    fn check_winner(&self, player: Player) -> bool {
60        // Check rows, columns, and diagonals for a win within a 3x3 cell
61        (0..BOARD_SIZE)
62            .any(|i| (0..BOARD_SIZE).all(|j| self.squares[i][j] == Square::Occupied(player)))
63            || (0..BOARD_SIZE)
64                .any(|j| (0..BOARD_SIZE).all(|i| self.squares[i][j] == Square::Occupied(player)))
65            || (0..BOARD_SIZE).all(|i| self.squares[i][i] == Square::Occupied(player))
66            || (0..BOARD_SIZE).all(|i| self.squares[i][2 - i] == Square::Occupied(player))
67    }
68    /// Get the winner of the game, if any
69    pub fn get_winner(&self) -> GameState {
70        if self.check_winner(Player::O) {
71            return GameState::Winner(Player::O);
72        }
73        if self.check_winner(Player::X) {
74            return GameState::Winner(Player::X);
75        }
76        // All cells are full
77        if self
78            .squares
79            .iter()
80            .all(|cols| cols.iter().all(|game| matches!(game, Square::Occupied(_))))
81        {
82            return GameState::Tie;
83        }
84        GameState::InProgress
85    }
86}
87
88/// Represents the 9x9 super Tic Tac Toe game. `X` starts
89///
90/// ## Example
91///
92/// ```
93/// # use super_ttt::{Game, Player};
94/// # fn main() {
95/// let mut game = Game::new();
96/// game.make_move(0, 0, 1, 1).unwrap();
97/// game.make_move(1, 1, 0, 0).unwrap();
98/// game.make_move(0, 0, 2, 2).unwrap();
99/// game.make_move(2, 2, 0, 2).unwrap();
100/// game.make_move(0, 2, 1, 0).unwrap();
101/// match game.get_winner() {
102///     super_ttt::GameState::Winner(player) => {
103///         println!("Player {:?} wins!", player);
104///     }
105///     super_ttt::GameState::Tie => {
106///         println!("It's a tie!");
107///     }
108///     super_ttt::GameState::InProgress => {
109///         println!("The game is still in progress.");
110///     }
111/// }
112/// # }
113/// ```
114///
115/// This is essentially just a game state
116/// with some relevant methods attached to it.
117#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
118pub struct Game {
119    /// Self explanatory. Public to allow implementations of display methods
120    pub boards: [[Board; BOARD_SIZE]; BOARD_SIZE],
121    /// The current player that will make the move when [`Game::make_move`] is called
122    pub current_player: Player,
123    /// The coordinates of the last move made by a player.
124    pub last_move_cords: Option<(usize, usize)>,
125}
126impl Default for Game {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131impl Display for Game {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "┏━━━┳━━━┳━━━┓\n")?;
134        for board_rows in 0..3 {
135            for cell_row in 0..3 {
136                write!(f, "┃")?;
137                for board in 0..3 {
138                    for cell_col in 0..3 {
139                        let cell = self.boards[board_rows][board].squares[cell_row][cell_col];
140                        let symbol = match cell {
141                            Square::Empty => " ",
142                            Square::Occupied(Player::X) => "X",
143                            Square::Occupied(Player::O) => "O",
144                        };
145                        write!(f, "{}", symbol)?;
146                    }
147                    write!(f, "┃")?;
148                }
149                write!(f, "\n")?;
150            }
151            if board_rows == 2 {
152                write!(f, "┗━━━┻━━━┻━━━┛\n")?;
153            } else {
154                write!(f, "┣━━━╋━━━╋━━━┫\n")?;
155            }
156        }
157        Ok(())
158    }
159}
160impl Game {
161    /// Create a new game. Default starting player is [`Player::X`]
162    pub fn new() -> Self {
163        Game {
164            boards: Default::default(),
165            current_player: Player::X,
166            last_move_cords: None,
167        }
168    }
169    /// Make a move on the game. This method will also swap the [`Game::current_player`]
170    pub fn make_move(
171        &mut self,
172        board_row: usize,
173        board_col: usize,
174        cell_row: usize,
175        cell_col: usize,
176    ) -> Result<Self, errors::InvalidMoveError> {
177        // Check if the move is valid
178        if self.boards[board_row][board_col].squares[cell_row][cell_col] != Square::Empty {
179            return Err(errors::InvalidMoveError::CellAlreadyOccupied);
180        }
181        if let Some((x, y)) = self.last_move_cords {
182            // X, Y is the coordinates of your opponent's last move
183            // If these coordinates don't match with the coordinates
184            // of the board that you want to put your piece in
185            // and the board with matching coordinates of (X, Y) hasn't
186            // finished, the move is illegal
187
188            // For example, let's say my opponent played in (0, 0, 2, 2)
189            // This means that they played in the top left board
190            // but the bottom right cell within that board.
191            // This means that my next move must be in the bottom right *board*
192            // unless that board has already been finished (there was a win or tie)
193            if (board_row, board_col) != (x, y)
194                && matches!(self.boards[x][y].get_winner(), GameState::InProgress)
195            {
196                return Err(errors::InvalidMoveError::InvalidBoard);
197            }
198        }
199
200        // Make the move
201        self.boards[board_row][board_col].squares[cell_row][cell_col] =
202            Square::Occupied(self.current_player);
203
204        // Switch to the next player
205        self.current_player = match self.current_player {
206            Player::X => Player::O,
207            Player::O => Player::X,
208        };
209
210        self.last_move_cords = Some((cell_row, cell_col));
211
212        Ok(*self)
213    }
214
215    /// Check if any of the boards has a winner
216    fn check_winner(&self, player: Player) -> bool {
217        // Columns
218        (0..BOARD_SIZE).any(|i| (0..BOARD_SIZE).all(|j| self.boards[i][j].get_winner() == GameState::Winner(player)))
219            || // Rows
220            (0..BOARD_SIZE).any(|j| {
221                (0..BOARD_SIZE).all(|i| self.boards[i][j].get_winner() == GameState::Winner(player))
222            })
223            ||  // y = -x Diagonals
224            (0..BOARD_SIZE).all(|i| self.boards[i][i].get_winner() == GameState::Winner(player))
225            || // y = x diagonals
226            (0..BOARD_SIZE).all(|i| self.boards[i][2 - i].get_winner() == GameState::Winner(player))
227    }
228    /// Get the winner of the game, if any
229    pub fn get_winner(&self) -> GameState {
230        if self.check_winner(Player::O) {
231            return GameState::Winner(Player::O);
232        }
233        if self.check_winner(Player::X) {
234            return GameState::Winner(Player::X);
235        }
236        // All boards have been finished
237        if self.boards.iter().all(|cols| {
238            cols.iter()
239                .all(|game| game.get_winner() != GameState::InProgress)
240        }) {
241            return GameState::Tie;
242        }
243        GameState::InProgress
244    }
245}
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use std::mem;
250
251    #[test]
252    fn game_struct_size() {
253        assert_eq!(mem::size_of::<Game>(), 112);
254    }
255}