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}