From a0a3449f04219707841c6774c8afbbd3e4453931 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 15:22:43 +0200 Subject: [PATCH 1/7] feat: init project Signed-off-by: Liam SOULET --- topics/tic-tac-toe/Cargo.toml | 6 ++++++ topics/tic-tac-toe/src/main.rs | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 topics/tic-tac-toe/Cargo.toml create mode 100644 topics/tic-tac-toe/src/main.rs diff --git a/topics/tic-tac-toe/Cargo.toml b/topics/tic-tac-toe/Cargo.toml new file mode 100644 index 0000000..a579fbf --- /dev/null +++ b/topics/tic-tac-toe/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 5e4b5f4d2628f8f3e89301e768df928910c9d14e Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 15:48:14 +0200 Subject: [PATCH 2/7] feat: add board and player sructs Signed-off-by: Liam SOULET --- topics/tic-tac-toe/Cargo.lock | 7 + topics/tic-tac-toe/src/board.rs | 240 ++++++++++++++++++++++++++++++++ topics/tic-tac-toe/src/main.rs | 20 ++- 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 topics/tic-tac-toe/Cargo.lock create mode 100644 topics/tic-tac-toe/src/board.rs diff --git a/topics/tic-tac-toe/Cargo.lock b/topics/tic-tac-toe/Cargo.lock new file mode 100644 index 0000000..32be7da --- /dev/null +++ b/topics/tic-tac-toe/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tic-tac-toe" +version = "0.1.0" diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs new file mode 100644 index 0000000..99603f2 --- /dev/null +++ b/topics/tic-tac-toe/src/board.rs @@ -0,0 +1,240 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Player { + X, + O, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cell { + Empty, + Occupied(Player), +} + +#[derive(Debug, Clone)] +pub struct Board { + /// Internal representation as a 1D array for simplicity + /// Positions 0-8 correspond to board positions 1-9 for user input + cells: [Cell; 9], +} + +impl Board { + pub fn new() -> Self { + Self { + cells: [Cell::Empty; 9], + } + } + + /// Places a player's mark at the specified position (1-9) + /// Returns true if the move was successful, false if invalid position or occupied + pub fn place_move(&mut self, position: usize, player: Player) -> bool { + // Validate position (1-9) + if position == 0 || position > 9 { + return false; + } + + let index = position - 1; + + if self.cells[index] == Cell::Empty { + self.cells[index] = Cell::Occupied(player); + true + } else { + false + } + } + + /// Checks if a position is empty + pub fn is_empty(&self, position: usize) -> bool { + if position == 0 || position > 9 { + return false; + } + self.cells[position - 1] == Cell::Empty + } + + /// Gets the cell at a specific position (1-9) + pub fn get_cell(&self, position: usize) -> Option { + if position == 0 || position > 9 { + return None; + } + Some(self.cells[position - 1]) + } + + /// Returns all empty positions (1-9) + pub fn get_empty_positions(&self) -> Vec { + self.cells + .iter() + .enumerate() + .filter_map(|(i, &cell)| { + if cell == Cell::Empty { + Some(i + 1) + } else { + None + } + }) + .collect() + } + + /// Checks if the board is full + pub fn is_full(&self) -> bool { + self.cells.iter().all(|&cell| cell != Cell::Empty) + } + + /// Displays the board in a readable format + pub fn display(&self) { + println!(); + for row in 0..3 { + for col in 0..3 { + let position = row * 3 + col + 1; + let display_char = match self.get_cell(position) { + Some(Cell::Empty) => " ".to_string(), + Some(Cell::Occupied(Player::X)) => "X".to_string(), + Some(Cell::Occupied(Player::O)) => "O".to_string(), + None => "?".to_string(), + }; + + print!(" {} ", display_char); + if col < 2 { + print!("|"); + } + } + println!(); + if row < 2 { + println!("-----------"); + } + } + println!(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_board() { + let board = Board::new(); + + for position in 1..=9 { + assert_eq!(board.get_cell(position), Some(Cell::Empty)); + assert!(board.is_empty(position)); + } + + assert!(!board.is_full()); + + assert_eq!(board.get_empty_positions().len(), 9); + assert_eq!(board.get_empty_positions(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]); + } + + #[test] + fn test_place_move_valid() { + let mut board = Board::new(); + + assert!(board.place_move(5, Player::X)); + assert_eq!(board.get_cell(5), Some(Cell::Occupied(Player::X))); + assert!(!board.is_empty(5)); + + assert!(board.place_move(1, Player::O)); + assert_eq!(board.get_cell(1), Some(Cell::Occupied(Player::O))); + assert!(!board.is_empty(1)); + + assert_eq!(board.get_empty_positions().len(), 7); + assert!(!board.get_empty_positions().contains(&1)); + assert!(!board.get_empty_positions().contains(&5)); + } + + #[test] + fn test_place_move_invalid_position() { + let mut board = Board::new(); + + assert!(!board.place_move(0, Player::X)); + assert!(!board.place_move(10, Player::X)); + + assert_eq!(board.get_empty_positions().len(), 9); + } + + #[test] + fn test_place_move_occupied_position() { + let mut board = Board::new(); + + assert!(board.place_move(5, Player::X)); + + assert!(!board.place_move(5, Player::O)); + + assert_eq!(board.get_cell(5), Some(Cell::Occupied(Player::X))); + } + + #[test] + fn test_get_cell_invalid_position() { + let board = Board::new(); + + assert_eq!(board.get_cell(0), None); + assert_eq!(board.get_cell(10), None); + } + + #[test] + fn test_is_empty_invalid_position() { + let board = Board::new(); + + assert!(!board.is_empty(0)); + assert!(!board.is_empty(10)); + } + + #[test] + fn test_is_full() { + let mut board = Board::new(); + + assert!(!board.is_full()); + + for position in 1..=9 { + let player = if position % 2 == 1 { Player::X } else { Player::O }; + board.place_move(position, player); + } + + assert!(board.is_full()); + assert_eq!(board.get_empty_positions().len(), 0); + } + + #[test] + fn test_get_empty_positions() { + let mut board = Board::new(); + + let mut expected = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; + assert_eq!(board.get_empty_positions(), expected); + + board.place_move(1, Player::X); + board.place_move(5, Player::O); + board.place_move(9, Player::X); + + expected.retain(|&x| x != 1 && x != 5 && x != 9); + assert_eq!(board.get_empty_positions(), expected); + assert_eq!(board.get_empty_positions(), vec![2, 3, 4, 6, 7, 8]); + } + + #[test] + fn test_board_clone() { + let mut original = Board::new(); + original.place_move(1, Player::X); + original.place_move(5, Player::O); + + let cloned = original.clone(); + + assert_eq!(cloned.get_cell(1), Some(Cell::Occupied(Player::X))); + assert_eq!(cloned.get_cell(5), Some(Cell::Occupied(Player::O))); + assert_eq!(cloned.get_empty_positions().len(), 7); + } + + #[test] + fn test_player_equality() { + assert_eq!(Player::X, Player::X); + assert_eq!(Player::O, Player::O); + assert_ne!(Player::X, Player::O); + } + + #[test] + fn test_cell_equality() { + assert_eq!(Cell::Empty, Cell::Empty); + assert_eq!(Cell::Occupied(Player::X), Cell::Occupied(Player::X)); + assert_eq!(Cell::Occupied(Player::O), Cell::Occupied(Player::O)); + assert_ne!(Cell::Empty, Cell::Occupied(Player::X)); + assert_ne!(Cell::Occupied(Player::X), Cell::Occupied(Player::O)); + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index e7a11a9..82aa957 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,3 +1,21 @@ +mod board; + +use board::{Board, Player}; + fn main() { - println!("Hello, world!"); + println!("=== Tic-Tac-Toe Board Demo ===\n"); + + let mut board = Board::new(); + + println!("Empty board:"); + board.display(); + + // Demo: Place some moves to show the board in action + println!("Demo: Placing some moves..."); + board.place_move(5, Player::X); + board.place_move(1, Player::O); + board.place_move(9, Player::X); + board.display(); + + println!("Run 'cargo test' to see all the unit tests for the board functionality!"); } From 9124b797ebdc715a5441cf20371ee2ddc57aa2e4 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 16:26:12 +0200 Subject: [PATCH 3/7] feat: add game state and win detection Signed-off-by: Liam SOULET --- topics/tic-tac-toe/src/game.rs | 421 +++++++++++++++++++++++++++++++++ topics/tic-tac-toe/src/main.rs | 31 ++- 2 files changed, 440 insertions(+), 12 deletions(-) create mode 100644 topics/tic-tac-toe/src/game.rs diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs new file mode 100644 index 0000000..c978c0c --- /dev/null +++ b/topics/tic-tac-toe/src/game.rs @@ -0,0 +1,421 @@ +use crate::board::{Board, Player, Cell}; + +/// Represents the game state +#[derive(Debug, PartialEq)] +pub enum GameState { + InProgress, + Won(Player), + Draw, +} + +/// Main game controller that manages the tic-tac-toe game logic +pub struct Game { + board: Board, + current_player: Player, + state: GameState, +} + +impl Game { + /// Creates a new game instance + pub fn new() -> Self { + Self { + board: Board::new(), + current_player: Player::X, // X always starts first + state: GameState::InProgress, + } + } + + /// Places a move for the current player at the specified position + /// Returns true if the move was successful, false otherwise + pub fn make_move(&mut self, position: usize) -> bool { + if self.state != GameState::InProgress { + return false; // Game is already over + } + + if self.board.place_move(position, self.current_player) { + self.update_game_state(); + if self.state == GameState::InProgress { + self.switch_player(); + } + true + } else { + false + } + } + + /// Gets the current game state + pub fn get_state(&self) -> &GameState { + &self.state + } + + /// Gets the current player + pub fn get_current_player(&self) -> Player { + self.current_player + } + + /// Gets a reference to the board + pub fn get_board(&self) -> &Board { + &self.board + } + + /// Checks if a position is valid and empty + pub fn is_valid_move(&self, position: usize) -> bool { + self.state == GameState::InProgress && self.board.is_empty(position) + } + + /// Gets all available moves + pub fn get_available_moves(&self) -> Vec { + if self.state != GameState::InProgress { + Vec::new() + } else { + self.board.get_empty_positions() + } + } + + /// Resets the game to initial state + pub fn reset(&mut self) { + self.board = Board::new(); + self.current_player = Player::X; + self.state = GameState::InProgress; + } + + /// Displays the current board + pub fn display_board(&self) { + self.board.display(); + } + + /// Checks if there's a winner and returns the winning player + pub fn check_winner(&self) -> Option { + // Winning combinations (indices 0-8) + let winning_combinations = [ + // Rows + [0, 1, 2], [3, 4, 5], [6, 7, 8], + // Columns + [0, 3, 6], [1, 4, 7], [2, 5, 8], + // Diagonals + [0, 4, 8], [2, 4, 6], + ]; + + for combo in &winning_combinations { + // Convert to 1-based positions for board.get_cell() + let pos1 = combo[0] + 1; + let pos2 = combo[1] + 1; + let pos3 = combo[2] + 1; + + if let (Some(Cell::Occupied(p1)), Some(Cell::Occupied(p2)), Some(Cell::Occupied(p3))) = + (self.board.get_cell(pos1), self.board.get_cell(pos2), self.board.get_cell(pos3)) { + if p1 == p2 && p2 == p3 { + return Some(p1); + } + } + } + None + } + + /// Checks if the game is over (either someone won or board is full) + pub fn is_game_over(&self) -> bool { + self.check_winner().is_some() || self.board.is_full() + } + + /// Updates the game state based on current board + fn update_game_state(&mut self) { + if let Some(winner) = self.check_winner() { + self.state = GameState::Won(winner); + } else if self.board.is_full() { + self.state = GameState::Draw; + } + } + + /// Switches to the next player + fn switch_player(&mut self) { + self.current_player = match self.current_player { + Player::X => Player::O, + Player::O => Player::X, + }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_game() { + let game = Game::new(); + assert_eq!(game.current_player, Player::X); + assert_eq!(game.state, GameState::InProgress); + assert_eq!(game.get_available_moves().len(), 9); + } + + #[test] + fn test_make_valid_move() { + let mut game = Game::new(); + + // X plays first + assert!(game.make_move(5)); + assert_eq!(game.current_player, Player::O); + assert_eq!(game.state, GameState::InProgress); + + // O plays next + assert!(game.make_move(1)); + assert_eq!(game.current_player, Player::X); + assert_eq!(game.state, GameState::InProgress); + } + + #[test] + fn test_make_invalid_move() { + let mut game = Game::new(); + + // Valid move first + assert!(game.make_move(5)); + + // Try to play same position + assert!(!game.make_move(5)); + assert_eq!(game.current_player, Player::O); // Should still be O's turn + + // Try invalid position + assert!(!game.make_move(0)); + assert!(!game.make_move(10)); + } + + #[test] + fn test_winning_game() { + let mut game = Game::new(); + + // X wins in top row + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(3); // X wins + + assert_eq!(game.state, GameState::Won(Player::X)); + assert_eq!(game.get_available_moves().len(), 0); + + // No more moves should be allowed + assert!(!game.make_move(6)); + } + + #[test] + fn test_draw_game() { + let mut game = Game::new(); + + // Create a true draw scenario - no three in a row for anyone + // X O X + // O X X + // O X O + game.make_move(1); // X + game.make_move(2); // O + game.make_move(3); // X + game.make_move(4); // O + game.make_move(5); // X + game.make_move(9); // O + game.make_move(6); // X + game.make_move(7); // O + game.make_move(8); // X + + assert_eq!(game.state, GameState::Draw); + assert_eq!(game.get_available_moves().len(), 0); + } + + #[test] + fn test_is_valid_move() { + let mut game = Game::new(); + + // All positions should be valid initially + for pos in 1..=9 { + assert!(game.is_valid_move(pos)); + } + + // After a move, that position should be invalid + game.make_move(5); + assert!(!game.is_valid_move(5)); + assert!(game.is_valid_move(1)); + + // Invalid positions + assert!(!game.is_valid_move(0)); + assert!(!game.is_valid_move(10)); + } + + #[test] + fn test_reset_game() { + let mut game = Game::new(); + + // Make some moves + game.make_move(1); + game.make_move(2); + game.make_move(3); + + // Reset + game.reset(); + + // Should be back to initial state + assert_eq!(game.current_player, Player::X); + assert_eq!(game.state, GameState::InProgress); + assert_eq!(game.get_available_moves().len(), 9); + } + + #[test] + fn test_game_state_after_win() { + let mut game = Game::new(); + + // X wins diagonally + game.make_move(1); // X + game.make_move(2); // O + game.make_move(5); // X + game.make_move(3); // O + game.make_move(9); // X wins + + assert_eq!(game.state, GameState::Won(Player::X)); + + // No more moves allowed after win + assert!(!game.is_valid_move(4)); + assert_eq!(game.get_available_moves().len(), 0); + } + + #[test] + fn test_check_winner_horizontal() { + let mut game = Game::new(); + + // Test first row win for X + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(3); // X wins + assert_eq!(game.check_winner(), Some(Player::X)); + + // Test second row win for O + let mut game = Game::new(); + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(7); // X + game.make_move(6); // O wins + assert_eq!(game.check_winner(), Some(Player::O)); + + // Test third row win for X + let mut game = Game::new(); + game.make_move(7); // X + game.make_move(1); // O + game.make_move(8); // X + game.make_move(2); // O + game.make_move(9); // X wins + assert_eq!(game.check_winner(), Some(Player::X)); + } + + #[test] + fn test_check_winner_vertical() { + // Test first column win for X + let mut game = Game::new(); + game.make_move(1); // X + game.make_move(2); // O + game.make_move(4); // X + game.make_move(3); // O + game.make_move(7); // X wins + assert_eq!(game.check_winner(), Some(Player::X)); + + // Test second column win for O + let mut game = Game::new(); + game.make_move(1); // X + game.make_move(2); // O + game.make_move(3); // X + game.make_move(5); // O + game.make_move(4); // X + game.make_move(8); // O wins + assert_eq!(game.check_winner(), Some(Player::O)); + + // Test third column win for X + let mut game = Game::new(); + game.make_move(3); // X + game.make_move(1); // O + game.make_move(6); // X + game.make_move(2); // O + game.make_move(9); // X wins + assert_eq!(game.check_winner(), Some(Player::X)); + } + + #[test] + fn test_check_winner_diagonal() { + // Test main diagonal win for X (top-left to bottom-right) + let mut game = Game::new(); + game.make_move(1); // X + game.make_move(2); // O + game.make_move(5); // X + game.make_move(3); // O + game.make_move(9); // X wins + assert_eq!(game.check_winner(), Some(Player::X)); + + // Test anti-diagonal win for O (top-right to bottom-left) + let mut game = Game::new(); + game.make_move(1); // X + game.make_move(3); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(6); // X + game.make_move(7); // O wins + assert_eq!(game.check_winner(), Some(Player::O)); + } + + #[test] + fn test_check_winner_no_winner() { + let mut game = Game::new(); + + // Empty game should have no winner + assert_eq!(game.check_winner(), None); + + // Incomplete lines should have no winner + game.make_move(1); // X + game.make_move(2); // O + game.make_move(4); // X + assert_eq!(game.check_winner(), None); + + // Mixed lines should have no winner + game.make_move(3); // O + assert_eq!(game.check_winner(), None); + } + + #[test] + fn test_is_game_over() { + let mut game = Game::new(); + + // Game should not be over at start + assert!(!game.is_game_over()); + + // Game should not be over with partial moves + game.make_move(1); // X + game.make_move(5); // O + assert!(!game.is_game_over()); + + // Game should be over when someone wins + game.make_move(2); // X + game.make_move(6); // O + game.make_move(3); // X wins + assert!(game.is_game_over()); + assert_eq!(game.check_winner(), Some(Player::X)); + } + + #[test] + fn test_is_game_over_full_board() { + let mut game = Game::new(); + + // Fill board without winner (draw scenario) + // X O X + // O X X + // O X O + game.make_move(1); // X + game.make_move(2); // O + game.make_move(3); // X + game.make_move(4); // O + game.make_move(5); // X + game.make_move(9); // O + game.make_move(6); // X + game.make_move(7); // O + game.make_move(8); // X + + assert!(game.is_game_over()); + assert_eq!(game.check_winner(), None); + assert!(game.get_board().is_full()); + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index 82aa957..16b3771 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,21 +1,28 @@ mod board; +mod game; -use board::{Board, Player}; +use game::{Game, GameState}; fn main() { - println!("=== Tic-Tac-Toe Board Demo ===\n"); + println!("=== Tic-Tac-Toe ==="); + println!("Simple demonstration of the game logic\n"); - let mut board = Board::new(); + let mut game = Game::new(); - println!("Empty board:"); - board.display(); + // Show initial empty board + println!("Initial board:"); + game.display_board(); - // Demo: Place some moves to show the board in action - println!("Demo: Placing some moves..."); - board.place_move(5, Player::X); - board.place_move(1, Player::O); - board.place_move(9, Player::X); - board.display(); + // Make a few moves to demonstrate + println!("Making some sample moves..."); + game.make_move(5); // X center + game.make_move(1); // O top-left + game.make_move(9); // X bottom-right - println!("Run 'cargo test' to see all the unit tests for the board functionality!"); + game.display_board(); + println!("Current player: {:?}", game.get_current_player()); + println!("Game state: {:?}", game.get_state()); + println!("Available moves: {:?}", game.get_available_moves()); + + println!("\nšŸŽ® Run 'cargo test' to see all unit tests!"); } From 800c34f0b8db253d205480609ea16cebd230c021 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 16:42:58 +0200 Subject: [PATCH 4/7] feat: add UI Signed-off-by: Liam SOULET --- topics/tic-tac-toe/src/main.rs | 25 +--- topics/tic-tac-toe/src/ui.rs | 241 +++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 topics/tic-tac-toe/src/ui.rs diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index 16b3771..a064a05 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,28 +1,9 @@ mod board; mod game; +mod ui; -use game::{Game, GameState}; +use ui::UI; fn main() { - println!("=== Tic-Tac-Toe ==="); - println!("Simple demonstration of the game logic\n"); - - let mut game = Game::new(); - - // Show initial empty board - println!("Initial board:"); - game.display_board(); - - // Make a few moves to demonstrate - println!("Making some sample moves..."); - game.make_move(5); // X center - game.make_move(1); // O top-left - game.make_move(9); // X bottom-right - - game.display_board(); - println!("Current player: {:?}", game.get_current_player()); - println!("Game state: {:?}", game.get_state()); - println!("Available moves: {:?}", game.get_available_moves()); - - println!("\nšŸŽ® Run 'cargo test' to see all unit tests!"); + UI::play_game(); } diff --git a/topics/tic-tac-toe/src/ui.rs b/topics/tic-tac-toe/src/ui.rs new file mode 100644 index 0000000..fd4e278 --- /dev/null +++ b/topics/tic-tac-toe/src/ui.rs @@ -0,0 +1,241 @@ +use crate::game::{Game, GameState}; +use crate::board::Player; +use std::io::{self, Write}; + +/// Handles user interface operations for the tic-tac-toe game +pub struct UI; + +impl UI { + /// Starts the interactive game loop + pub fn play_game() { + println!("šŸŽ® Welcome to Tic-Tac-Toe! šŸŽ®"); + println!("You are X, computer will be O."); + println!("Enter positions 1-9 corresponding to board positions:\n"); + + Self::show_position_guide(); + + loop { + let mut game = Game::new(); + Self::play_round(&mut game); + + if !Self::ask_play_again() { + println!("Thanks for playing! šŸ‘‹"); + break; + } + } + } + + /// Plays a single round of the game + fn play_round(game: &mut Game) { + println!("\nšŸ†• Starting new game!\n"); + + while *game.get_state() == GameState::InProgress { + // Display current board + game.display_board(); + + match game.get_current_player() { + Player::X => { + // Human player's turn + Self::handle_human_turn(game); + } + Player::O => { + // For now, let's make it human vs human + // Later we'll add AI here + Self::handle_human_turn_for_o(game); + } + } + } + + // Game over - show final state + game.display_board(); + Self::show_game_result(game.get_state()); + } + + /// Handles human player X's turn + fn handle_human_turn(game: &mut Game) { + loop { + let position = Self::get_user_input("Player X, enter your move (1-9): "); + + if game.make_move(position) { + break; + } else { + if position >= 1 && position <= 9 { + println!("āŒ Position {} is already occupied! Try again.", position); + } else { + println!("āŒ Invalid position! Please enter a number between 1 and 9."); + } + } + } + } + + /// Handles human player O's turn (temporary - will be replaced by AI) + fn handle_human_turn_for_o(game: &mut Game) { + loop { + let position = Self::get_user_input("Player O, enter your move (1-9): "); + + if game.make_move(position) { + break; + } else { + if position >= 1 && position <= 9 { + println!("āŒ Position {} is already occupied! Try again.", position); + } else { + println!("āŒ Invalid position! Please enter a number between 1 and 9."); + } + } + } + } + + /// Gets user input and validates it + fn get_user_input(prompt: &str) -> usize { + loop { + print!("{}", prompt); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + match io::stdin().read_line(&mut input) { + Ok(_) => { + match input.trim().parse::() { + Ok(num) => return num, + Err(_) => { + println!("āŒ Please enter a valid number!"); + } + } + } + Err(_) => { + println!("āŒ Error reading input! Please try again."); + } + } + } + } + + /// Shows the position guide to help users understand the board layout + fn show_position_guide() { + println!("Board positions:"); + println!(" 1 | 2 | 3 "); + println!("-----------"); + println!(" 4 | 5 | 6 "); + println!("-----------"); + println!(" 7 | 8 | 9 "); + println!(); + } + + /// Shows the game result + fn show_game_result(state: &GameState) { + match state { + GameState::Won(Player::X) => { + println!("šŸŽ‰ Congratulations! Player X wins! šŸŽ‰"); + } + GameState::Won(Player::O) => { + println!("šŸŽ‰ Congratulations! Player O wins! šŸŽ‰"); + } + GameState::Draw => { + println!("šŸ¤ It's a draw! Well played both players! šŸ¤"); + } + GameState::InProgress => { + println!("šŸ¤” Game is still in progress..."); + } + } + } + + /// Asks if the user wants to play again + fn ask_play_again() -> bool { + loop { + print!("\nšŸ”„ Would you like to play again? (y/n): "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + match io::stdin().read_line(&mut input) { + Ok(_) => { + match input.trim().to_lowercase().as_str() { + "y" | "yes" => return true, + "n" | "no" => return false, + _ => println!("āŒ Please enter 'y' for yes or 'n' for no."), + } + } + Err(_) => { + println!("āŒ Error reading input! Please try again."); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ui_game_flow_integration() { + // Test that UI can create and interact with game properly + let mut game = Game::new(); + + // Simulate a simple game sequence + assert_eq!(*game.get_state(), GameState::InProgress); + assert_eq!(game.get_current_player(), Player::X); + + // Make moves like UI would + assert!(game.make_move(1)); // X + assert_eq!(game.get_current_player(), Player::O); + + assert!(game.make_move(2)); // O + assert_eq!(game.get_current_player(), Player::X); + + // Verify game state is still in progress + assert_eq!(*game.get_state(), GameState::InProgress); + } + + #[test] + fn test_ui_handles_invalid_moves() { + let mut game = Game::new(); + + // Valid move first + assert!(game.make_move(5)); + + // UI should handle these invalid moves gracefully + assert!(!game.make_move(5)); // Already occupied + assert!(!game.make_move(0)); // Invalid position + assert!(!game.make_move(10)); // Invalid position + + // Player should still be O's turn after failed moves + assert_eq!(game.get_current_player(), Player::O); + } + + #[test] + fn test_ui_game_completion() { + let mut game = Game::new(); + + // Play a complete game (X wins) + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(3); // X wins + + // UI should recognize game is over + assert_eq!(*game.get_state(), GameState::Won(Player::X)); + + // No more moves should be possible + assert!(!game.make_move(6)); + assert!(!game.make_move(7)); + } + + #[test] + fn test_ui_draw_scenario() { + let mut game = Game::new(); + + // Play to a draw + game.make_move(1); // X + game.make_move(2); // O + game.make_move(3); // X + game.make_move(4); // O + game.make_move(5); // X + game.make_move(9); // O + game.make_move(6); // X + game.make_move(7); // O + game.make_move(8); // X + + // UI should recognize draw + assert_eq!(*game.get_state(), GameState::Draw); + assert!(!game.make_move(1)); // No more moves possible + } +} \ No newline at end of file From cb8a2990818a05747910363a9f00753f35971a99 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 16:57:38 +0200 Subject: [PATCH 5/7] feat: add minimax AI agent (and remove some comments) Signed-off-by: Liam SOULET --- topics/tic-tac-toe/src/ai.rs | 225 +++++++++++++++++++++++++++++++++ topics/tic-tac-toe/src/game.rs | 28 +--- topics/tic-tac-toe/src/main.rs | 2 + topics/tic-tac-toe/src/ui.rs | 53 +++----- 4 files changed, 248 insertions(+), 60 deletions(-) create mode 100644 topics/tic-tac-toe/src/ai.rs diff --git a/topics/tic-tac-toe/src/ai.rs b/topics/tic-tac-toe/src/ai.rs new file mode 100644 index 0000000..a76147d --- /dev/null +++ b/topics/tic-tac-toe/src/ai.rs @@ -0,0 +1,225 @@ +use crate::board::{Board, Player, Cell}; +use crate::game::Game; + +/// AI player that uses the Minimax algorithm to play optimally +pub struct AI { + player: Player, +} + +impl AI { + /// Creates a new AI player + pub fn new(player: Player) -> Self { + Self { player } + } + + /// Gets the best move for the AI using the Minimax algorithm + pub fn get_best_move(&self, game: &Game) -> Option { + let available_moves = game.get_available_moves(); + if available_moves.is_empty() { + return None; + } + + let mut best_score = i32::MIN; + let mut best_move = None; + + for &position in &available_moves { + let mut game_copy = self.clone_game(game); + + game_copy.make_move(position); + + let score = self.minimax(&game_copy, 0, false); + + if score > best_score { + best_score = score; + best_move = Some(position); + } + } + + best_move + } + + /// Minimax algorithm implementation with depth-first search + /// Returns the score for the current game state from the AI's perspective + /// is_maximizing: true when it's AI's turn (maximize), false when opponent's turn (minimize) + fn minimax(&self, game: &Game, depth: u32, is_maximizing: bool) -> i32 { + // Check terminal states (game over) + if let Some(winner) = game.check_winner() { + return if winner == self.player { + 10 - depth as i32 // AI wins: prefer winning sooner (higher score for fewer moves) + } else { + depth as i32 - 10 // AI loses: prefer losing later (less negative score) + }; + } + + if game.is_game_over() { + return 0; + } + + let available_moves = game.get_available_moves(); + + if is_maximizing { + // AI's turn - maximize the score + let mut max_score = i32::MIN; + + for &position in &available_moves { + let mut game_copy = self.clone_game(game); + game_copy.make_move(position); + + let score = self.minimax(&game_copy, depth + 1, false); + max_score = max_score.max(score); + } + + max_score + } else { + // Opponent's turn - minimize the score (from AI's perspective) + let mut min_score = i32::MAX; + + for &position in &available_moves { + let mut game_copy = self.clone_game(game); + game_copy.make_move(position); + + let score = self.minimax(&game_copy, depth + 1, true); + min_score = min_score.min(score); + } + + min_score + } + } + + /// Creates a copy of the game state for simulation + /// This is necessary since Game doesn't implement Clone + fn clone_game(&self, game: &Game) -> Game { + let mut new_game = Game::new(); + + let board = game.get_board(); + + let mut moves = Vec::new(); + for position in 1..=9 { + if let Some(Cell::Occupied(player)) = board.get_cell(position) { + moves.push((position, player)); + } + } + + + let mut x_moves = Vec::new(); + let mut o_moves = Vec::new(); + + for (pos, player) in moves { + match player { + Player::X => x_moves.push(pos), + Player::O => o_moves.push(pos), + } + } + + let mut move_sequence = Vec::new(); + let max_len = x_moves.len().max(o_moves.len()); + + for i in 0..max_len { + if i < x_moves.len() { + move_sequence.push(x_moves[i]); + } + if i < o_moves.len() { + move_sequence.push(o_moves[i]); + } + } + + for &position in &move_sequence { + new_game.make_move(position); + } + + new_game + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::game::Game; + + #[test] + fn test_ai_creation() { + let ai = AI::new(Player::O); + assert_eq!(ai.player, Player::O); + + let ai_x = AI::new(Player::X); + assert_eq!(ai_x.player, Player::X); + } + + #[test] + fn test_ai_blocks_winning_move() { + let mut game = Game::new(); + let ai = AI::new(Player::O); + + // X threatens to win in top row + game.make_move(1); // X + game.make_move(4); // O (AI's previous move) + game.make_move(2); // X + // Now X threatens to win at position 3 + + let ai_move = ai.get_best_move(&game); + assert_eq!(ai_move, Some(3)); // AI should block at position 3 + } + + #[test] + fn test_ai_takes_winning_move() { + let mut game = Game::new(); + let ai = AI::new(Player::O); + + // Set up a scenario where AI can win + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(7); // X + // Now O can win at position 6 + + let ai_move = ai.get_best_move(&game); + assert_eq!(ai_move, Some(6)); // AI should win at position 6 + } + + #[test] + fn test_ai_chooses_valid_move_on_empty_board() { + let game = Game::new(); + let ai = AI::new(Player::X); + + let ai_move = ai.get_best_move(&game); + // On an empty board, any move should be valid (1-9) + // The AI should choose some valid position + assert!(ai_move.is_some()); + let position = ai_move.unwrap(); + assert!(position >= 1 && position <= 9); + } + + #[test] + fn test_ai_no_moves_available() { + let mut game = Game::new(); + let ai = AI::new(Player::O); + + // Fill the board completely + for position in 1..=9 { + let player = if position % 2 == 1 { Player::X } else { Player::O }; + game.make_move(position); + } + + let ai_move = ai.get_best_move(&game); + assert_eq!(ai_move, None); // No moves available + } + + #[test] + fn test_minimax_prefers_winning_sooner() { + let mut game = Game::new(); + let ai = AI::new(Player::O); + + // Create a scenario where AI has multiple ways to win + // AI should prefer the immediate win over a longer path to victory + game.make_move(1); // X + game.make_move(4); // O + game.make_move(2); // X + game.make_move(5); // O + game.make_move(8); // X + // AI can win immediately at position 6 + + let ai_move = ai.get_best_move(&game); + assert_eq!(ai_move, Some(6)); // Should take the immediate win + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs index c978c0c..bfda1d2 100644 --- a/topics/tic-tac-toe/src/game.rs +++ b/topics/tic-tac-toe/src/game.rs @@ -29,7 +29,7 @@ impl Game { /// Returns true if the move was successful, false otherwise pub fn make_move(&mut self, position: usize) -> bool { if self.state != GameState::InProgress { - return false; // Game is already over + return false; } if self.board.place_move(position, self.current_player) { @@ -86,7 +86,6 @@ impl Game { /// Checks if there's a winner and returns the winning player pub fn check_winner(&self) -> Option { - // Winning combinations (indices 0-8) let winning_combinations = [ // Rows [0, 1, 2], [3, 4, 5], [6, 7, 8], @@ -97,7 +96,6 @@ impl Game { ]; for combo in &winning_combinations { - // Convert to 1-based positions for board.get_cell() let pos1 = combo[0] + 1; let pos2 = combo[1] + 1; let pos3 = combo[2] + 1; @@ -171,7 +169,7 @@ mod tests { // Try to play same position assert!(!game.make_move(5)); - assert_eq!(game.current_player, Player::O); // Should still be O's turn + assert_eq!(game.current_player, Player::O); // Try invalid position assert!(!game.make_move(0)); @@ -192,7 +190,6 @@ mod tests { assert_eq!(game.state, GameState::Won(Player::X)); assert_eq!(game.get_available_moves().len(), 0); - // No more moves should be allowed assert!(!game.make_move(6)); } @@ -222,12 +219,10 @@ mod tests { fn test_is_valid_move() { let mut game = Game::new(); - // All positions should be valid initially for pos in 1..=9 { assert!(game.is_valid_move(pos)); } - // After a move, that position should be invalid game.make_move(5); assert!(!game.is_valid_move(5)); assert!(game.is_valid_move(1)); @@ -241,15 +236,12 @@ mod tests { fn test_reset_game() { let mut game = Game::new(); - // Make some moves game.make_move(1); game.make_move(2); game.make_move(3); - // Reset game.reset(); - // Should be back to initial state assert_eq!(game.current_player, Player::X); assert_eq!(game.state, GameState::InProgress); assert_eq!(game.get_available_moves().len(), 9); @@ -268,7 +260,6 @@ mod tests { assert_eq!(game.state, GameState::Won(Player::X)); - // No more moves allowed after win assert!(!game.is_valid_move(4)); assert_eq!(game.get_available_moves().len(), 0); } @@ -277,7 +268,6 @@ mod tests { fn test_check_winner_horizontal() { let mut game = Game::new(); - // Test first row win for X game.make_move(1); // X game.make_move(4); // O game.make_move(2); // X @@ -285,7 +275,6 @@ mod tests { game.make_move(3); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - // Test second row win for O let mut game = Game::new(); game.make_move(1); // X game.make_move(4); // O @@ -295,7 +284,6 @@ mod tests { game.make_move(6); // O wins assert_eq!(game.check_winner(), Some(Player::O)); - // Test third row win for X let mut game = Game::new(); game.make_move(7); // X game.make_move(1); // O @@ -307,7 +295,6 @@ mod tests { #[test] fn test_check_winner_vertical() { - // Test first column win for X let mut game = Game::new(); game.make_move(1); // X game.make_move(2); // O @@ -316,7 +303,6 @@ mod tests { game.make_move(7); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - // Test second column win for O let mut game = Game::new(); game.make_move(1); // X game.make_move(2); // O @@ -326,7 +312,6 @@ mod tests { game.make_move(8); // O wins assert_eq!(game.check_winner(), Some(Player::O)); - // Test third column win for X let mut game = Game::new(); game.make_move(3); // X game.make_move(1); // O @@ -338,7 +323,6 @@ mod tests { #[test] fn test_check_winner_diagonal() { - // Test main diagonal win for X (top-left to bottom-right) let mut game = Game::new(); game.make_move(1); // X game.make_move(2); // O @@ -347,7 +331,6 @@ mod tests { game.make_move(9); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - // Test anti-diagonal win for O (top-right to bottom-left) let mut game = Game::new(); game.make_move(1); // X game.make_move(3); // O @@ -362,16 +345,13 @@ mod tests { fn test_check_winner_no_winner() { let mut game = Game::new(); - // Empty game should have no winner assert_eq!(game.check_winner(), None); - // Incomplete lines should have no winner game.make_move(1); // X game.make_move(2); // O game.make_move(4); // X assert_eq!(game.check_winner(), None); - // Mixed lines should have no winner game.make_move(3); // O assert_eq!(game.check_winner(), None); } @@ -380,15 +360,12 @@ mod tests { fn test_is_game_over() { let mut game = Game::new(); - // Game should not be over at start assert!(!game.is_game_over()); - // Game should not be over with partial moves game.make_move(1); // X game.make_move(5); // O assert!(!game.is_game_over()); - // Game should be over when someone wins game.make_move(2); // X game.make_move(6); // O game.make_move(3); // X wins @@ -400,7 +377,6 @@ mod tests { fn test_is_game_over_full_board() { let mut game = Game::new(); - // Fill board without winner (draw scenario) // X O X // O X X // O X O diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index a064a05..c761755 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,9 +1,11 @@ mod board; mod game; mod ui; +mod ai; use ui::UI; fn main() { + // Start the interactive game UI::play_game(); } diff --git a/topics/tic-tac-toe/src/ui.rs b/topics/tic-tac-toe/src/ui.rs index fd4e278..a5229c7 100644 --- a/topics/tic-tac-toe/src/ui.rs +++ b/topics/tic-tac-toe/src/ui.rs @@ -1,5 +1,6 @@ use crate::game::{Game, GameState}; use crate::board::Player; +use crate::ai::AI; use std::io::{self, Write}; /// Handles user interface operations for the tic-tac-toe game @@ -9,14 +10,15 @@ impl UI { /// Starts the interactive game loop pub fn play_game() { println!("šŸŽ® Welcome to Tic-Tac-Toe! šŸŽ®"); - println!("You are X, computer will be O."); + println!("You are X, AI is O."); println!("Enter positions 1-9 corresponding to board positions:\n"); Self::show_position_guide(); loop { let mut game = Game::new(); - Self::play_round(&mut game); + let ai = AI::new(Player::O); + Self::play_round(&mut game, &ai); if !Self::ask_play_again() { println!("Thanks for playing! šŸ‘‹"); @@ -26,11 +28,10 @@ impl UI { } /// Plays a single round of the game - fn play_round(game: &mut Game) { + fn play_round(game: &mut Game, ai: &AI) { println!("\nšŸ†• Starting new game!\n"); while *game.get_state() == GameState::InProgress { - // Display current board game.display_board(); match game.get_current_player() { @@ -39,14 +40,12 @@ impl UI { Self::handle_human_turn(game); } Player::O => { - // For now, let's make it human vs human - // Later we'll add AI here - Self::handle_human_turn_for_o(game); + // AI's turn + Self::handle_ai_turn(game, ai); } } } - // Game over - show final state game.display_board(); Self::show_game_result(game.get_state()); } @@ -54,7 +53,7 @@ impl UI { /// Handles human player X's turn fn handle_human_turn(game: &mut Game) { loop { - let position = Self::get_user_input("Player X, enter your move (1-9): "); + let position = Self::get_user_input("Your move (1-9): "); if game.make_move(position) { break; @@ -68,20 +67,18 @@ impl UI { } } - /// Handles human player O's turn (temporary - will be replaced by AI) - fn handle_human_turn_for_o(game: &mut Game) { - loop { - let position = Self::get_user_input("Player O, enter your move (1-9): "); + /// Handles AI's turn + fn handle_ai_turn(game: &mut Game, ai: &AI) { + println!("šŸ¤– AI is thinking..."); + + if let Some(position) = ai.get_best_move(game) { + println!("šŸ¤– AI plays position {}", position); - if game.make_move(position) { - break; - } else { - if position >= 1 && position <= 9 { - println!("āŒ Position {} is already occupied! Try again.", position); - } else { - println!("āŒ Invalid position! Please enter a number between 1 and 9."); - } + if !game.make_move(position) { + println!("āŒ AI error: Failed to make move. This shouldn't happen!"); } + } else { + println!("āŒ AI error: No valid moves available. This shouldn't happen!"); } } @@ -166,21 +163,17 @@ mod tests { #[test] fn test_ui_game_flow_integration() { - // Test that UI can create and interact with game properly let mut game = Game::new(); - // Simulate a simple game sequence assert_eq!(*game.get_state(), GameState::InProgress); assert_eq!(game.get_current_player(), Player::X); - // Make moves like UI would assert!(game.make_move(1)); // X assert_eq!(game.get_current_player(), Player::O); assert!(game.make_move(2)); // O assert_eq!(game.get_current_player(), Player::X); - // Verify game state is still in progress assert_eq!(*game.get_state(), GameState::InProgress); } @@ -188,15 +181,12 @@ mod tests { fn test_ui_handles_invalid_moves() { let mut game = Game::new(); - // Valid move first assert!(game.make_move(5)); - // UI should handle these invalid moves gracefully assert!(!game.make_move(5)); // Already occupied assert!(!game.make_move(0)); // Invalid position assert!(!game.make_move(10)); // Invalid position - // Player should still be O's turn after failed moves assert_eq!(game.get_current_player(), Player::O); } @@ -204,17 +194,14 @@ mod tests { fn test_ui_game_completion() { let mut game = Game::new(); - // Play a complete game (X wins) game.make_move(1); // X game.make_move(4); // O game.make_move(2); // X game.make_move(5); // O game.make_move(3); // X wins - // UI should recognize game is over assert_eq!(*game.get_state(), GameState::Won(Player::X)); - // No more moves should be possible assert!(!game.make_move(6)); assert!(!game.make_move(7)); } @@ -223,7 +210,6 @@ mod tests { fn test_ui_draw_scenario() { let mut game = Game::new(); - // Play to a draw game.make_move(1); // X game.make_move(2); // O game.make_move(3); // X @@ -234,8 +220,7 @@ mod tests { game.make_move(7); // O game.make_move(8); // X - // UI should recognize draw assert_eq!(*game.get_state(), GameState::Draw); - assert!(!game.make_move(1)); // No more moves possible + assert!(!game.make_move(1)); } } \ No newline at end of file From 3691bf92f4cd0d086898f1e8ce2e761a1f7045c2 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 17:02:41 +0200 Subject: [PATCH 6/7] docs: add architecture markdown document Signed-off-by: Liam SOULET --- topics/tic-tac-toe/docs/architecture.md | 221 ++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 topics/tic-tac-toe/docs/architecture.md diff --git a/topics/tic-tac-toe/docs/architecture.md b/topics/tic-tac-toe/docs/architecture.md new file mode 100644 index 0000000..e2f53c1 --- /dev/null +++ b/topics/tic-tac-toe/docs/architecture.md @@ -0,0 +1,221 @@ +# Tic-Tac-Toe AI Agent - Architecture Document + +## Project Definition + +This project implements a command-line Tic-Tac-Toe game where a human player competes against an AI opponent. The AI uses the Minimax algorithm with depth-first search to play optimally, ensuring it never loses and will either win or draw every game. + +### Goals +- Create an interactive command-line Tic-Tac-Toe game +- Implement an unbeatable AI using the Minimax algorithm +- Provide a clean, modular architecture with clear separation of concerns +- Ensure the code is well-documented, tested, and follows Rust best practices +- Build a system that can be easily extended or modified + +## Components and Modules + +The project is architected using a layered, modular approach with clear separation of responsibilities: + +### Core Modules + +#### 1. **Board Module (`board.rs`)** +**Responsibility**: Physical game state representation and basic operations +- Represents the 3x3 game board as a 1D array for efficiency +- Manages cell states (Empty, Occupied by Player X/O) +- Handles move placement and position validation +- Provides board querying methods (empty positions, full board check) +- Implements clean display formatting + +#### 2. **Game Module (`game.rs`)** +**Responsibility**: Game logic, rules, and state management +- Manages overall game state (InProgress, Won, Draw) +- Handles player turns and move validation +- Implements win condition detection using pattern matching +- Provides game flow control (reset, state transitions) +- Acts as the central coordinator between other modules + +#### 3. **AI Module (`ai.rs`)** +**Responsibility**: Intelligent opponent using Minimax algorithm +- Implements the Minimax algorithm with depth-first search +- Evaluates all possible game states recursively +- Scores positions: +10 for AI win, -10 for AI loss, 0 for draw +- Optimizes for winning quickly and losing slowly +- Provides unbeatable gameplay that never loses + +#### 4. **UI Module (`ui.rs`)** +**Responsibility**: User interaction and interface management +- Handles command-line input/output operations +- Manages game flow (start, play rounds, restart) +- Provides user-friendly error messages and feedback +- Displays board state and position guides +- Coordinates between human input and AI responses + +#### 5. **Main Application (`main.rs`)** +**Responsibility**: Entry point and module coordination +- Orchestrates all modules +- Initializes the game system +- Provides the main execution flow + +### Architecture Justification + +This modular design follows several key principles: + +**1. Single Responsibility Principle**: Each module has one clear purpose +- Board: Data representation +- Game: Business logic +- AI: Intelligence algorithms +- UI: User interaction + +**2. Separation of Concerns**: Clear boundaries between layers +- Data layer (Board) is independent of game rules +- Game logic is separate from AI implementation +- UI is decoupled from core game mechanics + +**3. Dependency Direction**: Clean dependency flow +``` +main.rs → ui.rs → game.rs → board.rs + → ai.rs ↗ +``` + +**4. Testability**: Each module can be unit tested independently +- 11 tests for Board operations +- 14 tests for Game logic +- 6 tests for AI behavior +- 4 integration tests for UI + +**5. Extensibility**: Easy to modify or extend +- AI algorithm can be swapped without affecting other modules +- UI can be replaced (web, GUI) without changing core logic +- Game rules can be modified independently + +## Usage + +### Building and Running + +```bash +# Navigate to project directory +cd topics/tic-tac-toe + +# Build the project +cargo build + +# Run the game +cargo run + +# Run tests +cargo test +``` + +### Gameplay Experience + +#### 1. **Game Start** +The game displays a welcome message and position guide: +``` +šŸŽ® Welcome to Tic-Tac-Toe! šŸŽ® +You are X, AI is O. +Enter positions 1-9 corresponding to board positions: + +Board positions: + 1 | 2 | 3 +----------- + 4 | 5 | 6 +----------- + 7 | 8 | 9 +``` + +#### 2. **Gameplay Flow** +- Human player (X) always goes first +- Enter positions 1-9 to place your mark +- AI automatically responds with optimal moves +- Game displays board state after each move +- Clear feedback for invalid moves or occupied positions + +#### 3. **Example Game Session** +``` +šŸ†• Starting new game! + + | | +----------- + | | +----------- + | | + +Your move (1-9): 5 + + | | +----------- + | X | +----------- + | | + +šŸ¤– AI is thinking... +šŸ¤– AI plays position 1 + + O | | +----------- + | X | +----------- + | | + +Your move (1-9): 9 +``` + +#### 4. **Game Results** +The game automatically detects and displays results: +- **Human Win**: "šŸŽ‰ Congratulations! Player X wins! šŸŽ‰" +- **AI Win**: "šŸŽ‰ Congratulations! Player O wins! šŸŽ‰" +- **Draw**: "šŸ¤ It's a draw! Well played both players! šŸ¤" + +#### 5. **Replay Option** +After each game, players can choose to play again: +``` +šŸ”„ Would you like to play again? (y/n): y +``` + +### AI Behavior + +The AI implements optimal Minimax strategy: +- **Defensive**: Automatically blocks human winning moves +- **Offensive**: Takes immediate winning opportunities +- **Strategic**: Chooses moves that maximize long-term advantage +- **Unbeatable**: Mathematical guarantee of never losing + +### Development and Testing + +#### Running Tests +```bash +# Run all tests (35 total) +cargo test + +# Run specific module tests +cargo test board::tests +cargo test game::tests +cargo test ai::tests +cargo test ui::tests + +# Run with verbose output +cargo test -- --nocapture +``` + +#### Code Quality +```bash +# Check for compilation issues +cargo check + +# Format code +cargo fmt + +# Run clippy for additional lints +cargo clippy +``` + +### Technical Specifications + +- **Language**: Rust (Edition 2024) +- **Dependencies**: Standard library only +- **Board Representation**: 1D array of 9 positions +- **AI Algorithm**: Minimax with depth-first search +- **Input Validation**: Comprehensive error handling +- **Test Coverage**: 35 unit and integration tests +- **Performance**: Instant AI response for all game states + +This architecture provides a robust, maintainable, and extensible foundation for the Tic-Tac-Toe game while demonstrating clean code principles and effective modular design. \ No newline at end of file From 0076a6dd9f4e9565b12ee45b14861f1d16b6fec8 Mon Sep 17 00:00:00 2001 From: Liam SOULET Date: Thu, 23 Oct 2025 17:18:19 +0200 Subject: [PATCH 7/7] fix: remove dead functions and unused imports Signed-off-by: Liam SOULET --- topics/tic-tac-toe/src/ai.rs | 65 +++++++++-------- topics/tic-tac-toe/src/board.rs | 73 ++++++++----------- topics/tic-tac-toe/src/game.rs | 125 ++++++++++++-------------------- topics/tic-tac-toe/src/main.rs | 2 +- topics/tic-tac-toe/src/ui.rs | 94 +++++++++++------------- 5 files changed, 153 insertions(+), 206 deletions(-) diff --git a/topics/tic-tac-toe/src/ai.rs b/topics/tic-tac-toe/src/ai.rs index a76147d..2d4bf32 100644 --- a/topics/tic-tac-toe/src/ai.rs +++ b/topics/tic-tac-toe/src/ai.rs @@ -1,4 +1,4 @@ -use crate::board::{Board, Player, Cell}; +use crate::board::{Cell, Player}; use crate::game::Game; /// AI player that uses the Minimax algorithm to play optimally @@ -24,11 +24,11 @@ impl AI { for &position in &available_moves { let mut game_copy = self.clone_game(game); - + game_copy.make_move(position); - + let score = self.minimax(&game_copy, 0, false); - + if score > best_score { best_score = score; best_move = Some(position); @@ -45,9 +45,9 @@ impl AI { // Check terminal states (game over) if let Some(winner) = game.check_winner() { return if winner == self.player { - 10 - depth as i32 // AI wins: prefer winning sooner (higher score for fewer moves) + 10 - depth as i32 // AI wins: prefer winning sooner (higher score for fewer moves) } else { - depth as i32 - 10 // AI loses: prefer losing later (less negative score) + depth as i32 - 10 // AI loses: prefer losing later (less negative score) }; } @@ -56,32 +56,32 @@ impl AI { } let available_moves = game.get_available_moves(); - + if is_maximizing { // AI's turn - maximize the score let mut max_score = i32::MIN; - + for &position in &available_moves { let mut game_copy = self.clone_game(game); game_copy.make_move(position); - + let score = self.minimax(&game_copy, depth + 1, false); max_score = max_score.max(score); } - + max_score } else { // Opponent's turn - minimize the score (from AI's perspective) let mut min_score = i32::MAX; - + for &position in &available_moves { let mut game_copy = self.clone_game(game); game_copy.make_move(position); - + let score = self.minimax(&game_copy, depth + 1, true); min_score = min_score.min(score); } - + min_score } } @@ -91,17 +91,16 @@ impl AI { fn clone_game(&self, game: &Game) -> Game { let mut new_game = Game::new(); - let board = game.get_board(); + let board = game.get_board(); - let mut moves = Vec::new(); + let mut moves = Vec::new(); for position in 1..=9 { if let Some(Cell::Occupied(player)) = board.get_cell(position) { moves.push((position, player)); } } - - let mut x_moves = Vec::new(); + let mut x_moves = Vec::new(); let mut o_moves = Vec::new(); for (pos, player) in moves { @@ -111,7 +110,7 @@ impl AI { } } - let mut move_sequence = Vec::new(); + let mut move_sequence = Vec::new(); let max_len = x_moves.len().max(o_moves.len()); for i in 0..max_len { @@ -123,7 +122,7 @@ impl AI { } } - for &position in &move_sequence { + for &position in &move_sequence { new_game.make_move(position); } @@ -140,7 +139,7 @@ mod tests { fn test_ai_creation() { let ai = AI::new(Player::O); assert_eq!(ai.player, Player::O); - + let ai_x = AI::new(Player::X); assert_eq!(ai_x.player, Player::X); } @@ -149,13 +148,13 @@ mod tests { fn test_ai_blocks_winning_move() { let mut game = Game::new(); let ai = AI::new(Player::O); - + // X threatens to win in top row game.make_move(1); // X game.make_move(4); // O (AI's previous move) game.make_move(2); // X // Now X threatens to win at position 3 - + let ai_move = ai.get_best_move(&game); assert_eq!(ai_move, Some(3)); // AI should block at position 3 } @@ -164,7 +163,7 @@ mod tests { fn test_ai_takes_winning_move() { let mut game = Game::new(); let ai = AI::new(Player::O); - + // Set up a scenario where AI can win game.make_move(1); // X game.make_move(4); // O @@ -172,7 +171,7 @@ mod tests { game.make_move(5); // O game.make_move(7); // X // Now O can win at position 6 - + let ai_move = ai.get_best_move(&game); assert_eq!(ai_move, Some(6)); // AI should win at position 6 } @@ -181,7 +180,7 @@ mod tests { fn test_ai_chooses_valid_move_on_empty_board() { let game = Game::new(); let ai = AI::new(Player::X); - + let ai_move = ai.get_best_move(&game); // On an empty board, any move should be valid (1-9) // The AI should choose some valid position @@ -194,13 +193,17 @@ mod tests { fn test_ai_no_moves_available() { let mut game = Game::new(); let ai = AI::new(Player::O); - + // Fill the board completely for position in 1..=9 { - let player = if position % 2 == 1 { Player::X } else { Player::O }; + let _player = if position % 2 == 1 { + Player::X + } else { + Player::O + }; game.make_move(position); } - + let ai_move = ai.get_best_move(&game); assert_eq!(ai_move, None); // No moves available } @@ -209,7 +212,7 @@ mod tests { fn test_minimax_prefers_winning_sooner() { let mut game = Game::new(); let ai = AI::new(Player::O); - + // Create a scenario where AI has multiple ways to win // AI should prefer the immediate win over a longer path to victory game.make_move(1); // X @@ -218,8 +221,8 @@ mod tests { game.make_move(5); // O game.make_move(8); // X // AI can win immediately at position 6 - + let ai_move = ai.get_best_move(&game); assert_eq!(ai_move, Some(6)); // Should take the immediate win } -} \ No newline at end of file +} diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs index 99603f2..addd129 100644 --- a/topics/tic-tac-toe/src/board.rs +++ b/topics/tic-tac-toe/src/board.rs @@ -31,9 +31,9 @@ impl Board { if position == 0 || position > 9 { return false; } - + let index = position - 1; - + if self.cells[index] == Cell::Empty { self.cells[index] = Cell::Occupied(player); true @@ -42,14 +42,6 @@ impl Board { } } - /// Checks if a position is empty - pub fn is_empty(&self, position: usize) -> bool { - if position == 0 || position > 9 { - return false; - } - self.cells[position - 1] == Cell::Empty - } - /// Gets the cell at a specific position (1-9) pub fn get_cell(&self, position: usize) -> Option { if position == 0 || position > 9 { @@ -90,7 +82,7 @@ impl Board { Some(Cell::Occupied(Player::O)) => "O".to_string(), None => "?".to_string(), }; - + print!(" {} ", display_char); if col < 2 { print!("|"); @@ -112,14 +104,13 @@ mod tests { #[test] fn test_new_board() { let board = Board::new(); - + for position in 1..=9 { assert_eq!(board.get_cell(position), Some(Cell::Empty)); - assert!(board.is_empty(position)); } - + assert!(!board.is_full()); - + assert_eq!(board.get_empty_positions().len(), 9); assert_eq!(board.get_empty_positions(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]); } @@ -127,15 +118,13 @@ mod tests { #[test] fn test_place_move_valid() { let mut board = Board::new(); - + assert!(board.place_move(5, Player::X)); assert_eq!(board.get_cell(5), Some(Cell::Occupied(Player::X))); - assert!(!board.is_empty(5)); - + assert!(board.place_move(1, Player::O)); assert_eq!(board.get_cell(1), Some(Cell::Occupied(Player::O))); - assert!(!board.is_empty(1)); - + assert_eq!(board.get_empty_positions().len(), 7); assert!(!board.get_empty_positions().contains(&1)); assert!(!board.get_empty_positions().contains(&5)); @@ -144,51 +133,47 @@ mod tests { #[test] fn test_place_move_invalid_position() { let mut board = Board::new(); - + assert!(!board.place_move(0, Player::X)); assert!(!board.place_move(10, Player::X)); - + assert_eq!(board.get_empty_positions().len(), 9); } #[test] fn test_place_move_occupied_position() { let mut board = Board::new(); - + assert!(board.place_move(5, Player::X)); - + assert!(!board.place_move(5, Player::O)); - + assert_eq!(board.get_cell(5), Some(Cell::Occupied(Player::X))); } #[test] fn test_get_cell_invalid_position() { let board = Board::new(); - + assert_eq!(board.get_cell(0), None); assert_eq!(board.get_cell(10), None); } - #[test] - fn test_is_empty_invalid_position() { - let board = Board::new(); - - assert!(!board.is_empty(0)); - assert!(!board.is_empty(10)); - } - #[test] fn test_is_full() { let mut board = Board::new(); - + assert!(!board.is_full()); - + for position in 1..=9 { - let player = if position % 2 == 1 { Player::X } else { Player::O }; + let player = if position % 2 == 1 { + Player::X + } else { + Player::O + }; board.place_move(position, player); } - + assert!(board.is_full()); assert_eq!(board.get_empty_positions().len(), 0); } @@ -196,14 +181,14 @@ mod tests { #[test] fn test_get_empty_positions() { let mut board = Board::new(); - + let mut expected = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; assert_eq!(board.get_empty_positions(), expected); - + board.place_move(1, Player::X); board.place_move(5, Player::O); board.place_move(9, Player::X); - + expected.retain(|&x| x != 1 && x != 5 && x != 9); assert_eq!(board.get_empty_positions(), expected); assert_eq!(board.get_empty_positions(), vec![2, 3, 4, 6, 7, 8]); @@ -214,9 +199,9 @@ mod tests { let mut original = Board::new(); original.place_move(1, Player::X); original.place_move(5, Player::O); - + let cloned = original.clone(); - + assert_eq!(cloned.get_cell(1), Some(Cell::Occupied(Player::X))); assert_eq!(cloned.get_cell(5), Some(Cell::Occupied(Player::O))); assert_eq!(cloned.get_empty_positions().len(), 7); @@ -237,4 +222,4 @@ mod tests { assert_ne!(Cell::Empty, Cell::Occupied(Player::X)); assert_ne!(Cell::Occupied(Player::X), Cell::Occupied(Player::O)); } -} \ No newline at end of file +} diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs index bfda1d2..3f3b231 100644 --- a/topics/tic-tac-toe/src/game.rs +++ b/topics/tic-tac-toe/src/game.rs @@ -1,4 +1,4 @@ -use crate::board::{Board, Player, Cell}; +use crate::board::{Board, Cell, Player}; /// Represents the game state #[derive(Debug, PartialEq)] @@ -58,11 +58,6 @@ impl Game { &self.board } - /// Checks if a position is valid and empty - pub fn is_valid_move(&self, position: usize) -> bool { - self.state == GameState::InProgress && self.board.is_empty(position) - } - /// Gets all available moves pub fn get_available_moves(&self) -> Vec { if self.state != GameState::InProgress { @@ -72,13 +67,6 @@ impl Game { } } - /// Resets the game to initial state - pub fn reset(&mut self) { - self.board = Board::new(); - self.current_player = Player::X; - self.state = GameState::InProgress; - } - /// Displays the current board pub fn display_board(&self) { self.board.display(); @@ -88,20 +76,28 @@ impl Game { pub fn check_winner(&self) -> Option { let winning_combinations = [ // Rows - [0, 1, 2], [3, 4, 5], [6, 7, 8], + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], // Columns - [0, 3, 6], [1, 4, 7], [2, 5, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], // Diagonals - [0, 4, 8], [2, 4, 6], + [0, 4, 8], + [2, 4, 6], ]; for combo in &winning_combinations { let pos1 = combo[0] + 1; let pos2 = combo[1] + 1; let pos3 = combo[2] + 1; - - if let (Some(Cell::Occupied(p1)), Some(Cell::Occupied(p2)), Some(Cell::Occupied(p3))) = - (self.board.get_cell(pos1), self.board.get_cell(pos2), self.board.get_cell(pos3)) { + + if let (Some(Cell::Occupied(p1)), Some(Cell::Occupied(p2)), Some(Cell::Occupied(p3))) = ( + self.board.get_cell(pos1), + self.board.get_cell(pos2), + self.board.get_cell(pos3), + ) { if p1 == p2 && p2 == p3 { return Some(p1); } @@ -148,12 +144,12 @@ mod tests { #[test] fn test_make_valid_move() { let mut game = Game::new(); - + // X plays first assert!(game.make_move(5)); assert_eq!(game.current_player, Player::O); assert_eq!(game.state, GameState::InProgress); - + // O plays next assert!(game.make_move(1)); assert_eq!(game.current_player, Player::X); @@ -163,14 +159,14 @@ mod tests { #[test] fn test_make_invalid_move() { let mut game = Game::new(); - + // Valid move first assert!(game.make_move(5)); - + // Try to play same position assert!(!game.make_move(5)); assert_eq!(game.current_player, Player::O); - + // Try invalid position assert!(!game.make_move(0)); assert!(!game.make_move(10)); @@ -179,24 +175,24 @@ mod tests { #[test] fn test_winning_game() { let mut game = Game::new(); - + // X wins in top row game.make_move(1); // X game.make_move(4); // O game.make_move(2); // X game.make_move(5); // O game.make_move(3); // X wins - + assert_eq!(game.state, GameState::Won(Player::X)); assert_eq!(game.get_available_moves().len(), 0); - + assert!(!game.make_move(6)); } #[test] fn test_draw_game() { let mut game = Game::new(); - + // Create a true draw scenario - no three in a row for anyone // X O X // O X X @@ -210,71 +206,40 @@ mod tests { game.make_move(6); // X game.make_move(7); // O game.make_move(8); // X - + assert_eq!(game.state, GameState::Draw); assert_eq!(game.get_available_moves().len(), 0); } - #[test] - fn test_is_valid_move() { - let mut game = Game::new(); - - for pos in 1..=9 { - assert!(game.is_valid_move(pos)); - } - - game.make_move(5); - assert!(!game.is_valid_move(5)); - assert!(game.is_valid_move(1)); - - // Invalid positions - assert!(!game.is_valid_move(0)); - assert!(!game.is_valid_move(10)); - } - - #[test] - fn test_reset_game() { - let mut game = Game::new(); - - game.make_move(1); - game.make_move(2); - game.make_move(3); - - game.reset(); - - assert_eq!(game.current_player, Player::X); - assert_eq!(game.state, GameState::InProgress); - assert_eq!(game.get_available_moves().len(), 9); - } - #[test] fn test_game_state_after_win() { let mut game = Game::new(); - + // X wins diagonally game.make_move(1); // X game.make_move(2); // O game.make_move(5); // X game.make_move(3); // O game.make_move(9); // X wins - + assert_eq!(game.state, GameState::Won(Player::X)); - - assert!(!game.is_valid_move(4)); + + // No more moves allowed after win + assert!(!game.make_move(4)); assert_eq!(game.get_available_moves().len(), 0); } #[test] fn test_check_winner_horizontal() { let mut game = Game::new(); - + game.make_move(1); // X game.make_move(4); // O game.make_move(2); // X game.make_move(5); // O game.make_move(3); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - + let mut game = Game::new(); game.make_move(1); // X game.make_move(4); // O @@ -283,7 +248,7 @@ mod tests { game.make_move(7); // X game.make_move(6); // O wins assert_eq!(game.check_winner(), Some(Player::O)); - + let mut game = Game::new(); game.make_move(7); // X game.make_move(1); // O @@ -302,7 +267,7 @@ mod tests { game.make_move(3); // O game.make_move(7); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - + let mut game = Game::new(); game.make_move(1); // X game.make_move(2); // O @@ -311,7 +276,7 @@ mod tests { game.make_move(4); // X game.make_move(8); // O wins assert_eq!(game.check_winner(), Some(Player::O)); - + let mut game = Game::new(); game.make_move(3); // X game.make_move(1); // O @@ -330,7 +295,7 @@ mod tests { game.make_move(3); // O game.make_move(9); // X wins assert_eq!(game.check_winner(), Some(Player::X)); - + let mut game = Game::new(); game.make_move(1); // X game.make_move(3); // O @@ -344,14 +309,14 @@ mod tests { #[test] fn test_check_winner_no_winner() { let mut game = Game::new(); - + assert_eq!(game.check_winner(), None); - + game.make_move(1); // X game.make_move(2); // O game.make_move(4); // X assert_eq!(game.check_winner(), None); - + game.make_move(3); // O assert_eq!(game.check_winner(), None); } @@ -359,13 +324,13 @@ mod tests { #[test] fn test_is_game_over() { let mut game = Game::new(); - + assert!(!game.is_game_over()); - + game.make_move(1); // X game.make_move(5); // O assert!(!game.is_game_over()); - + game.make_move(2); // X game.make_move(6); // O game.make_move(3); // X wins @@ -376,7 +341,7 @@ mod tests { #[test] fn test_is_game_over_full_board() { let mut game = Game::new(); - + // X O X // O X X // O X O @@ -389,9 +354,9 @@ mod tests { game.make_move(6); // X game.make_move(7); // O game.make_move(8); // X - + assert!(game.is_game_over()); assert_eq!(game.check_winner(), None); assert!(game.get_board().is_full()); } -} \ No newline at end of file +} diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index c761755..68278a9 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,7 +1,7 @@ +mod ai; mod board; mod game; mod ui; -mod ai; use ui::UI; diff --git a/topics/tic-tac-toe/src/ui.rs b/topics/tic-tac-toe/src/ui.rs index a5229c7..9a1589d 100644 --- a/topics/tic-tac-toe/src/ui.rs +++ b/topics/tic-tac-toe/src/ui.rs @@ -1,6 +1,6 @@ -use crate::game::{Game, GameState}; -use crate::board::Player; use crate::ai::AI; +use crate::board::Player; +use crate::game::{Game, GameState}; use std::io::{self, Write}; /// Handles user interface operations for the tic-tac-toe game @@ -12,14 +12,14 @@ impl UI { println!("šŸŽ® Welcome to Tic-Tac-Toe! šŸŽ®"); println!("You are X, AI is O."); println!("Enter positions 1-9 corresponding to board positions:\n"); - + Self::show_position_guide(); - + loop { let mut game = Game::new(); let ai = AI::new(Player::O); Self::play_round(&mut game, &ai); - + if !Self::ask_play_again() { println!("Thanks for playing! šŸ‘‹"); break; @@ -30,10 +30,10 @@ impl UI { /// Plays a single round of the game fn play_round(game: &mut Game, ai: &AI) { println!("\nšŸ†• Starting new game!\n"); - + while *game.get_state() == GameState::InProgress { game.display_board(); - + match game.get_current_player() { Player::X => { // Human player's turn @@ -45,7 +45,7 @@ impl UI { } } } - + game.display_board(); Self::show_game_result(game.get_state()); } @@ -54,15 +54,13 @@ impl UI { fn handle_human_turn(game: &mut Game) { loop { let position = Self::get_user_input("Your move (1-9): "); - + if game.make_move(position) { break; + } else if (1..=9).contains(&position) { + println!("āŒ Position {} is already occupied! Try again.", position); } else { - if position >= 1 && position <= 9 { - println!("āŒ Position {} is already occupied! Try again.", position); - } else { - println!("āŒ Invalid position! Please enter a number between 1 and 9."); - } + println!("āŒ Invalid position! Please enter a number between 1 and 9."); } } } @@ -70,10 +68,10 @@ impl UI { /// Handles AI's turn fn handle_ai_turn(game: &mut Game, ai: &AI) { println!("šŸ¤– AI is thinking..."); - + if let Some(position) = ai.get_best_move(game) { println!("šŸ¤– AI plays position {}", position); - + if !game.make_move(position) { println!("āŒ AI error: Failed to make move. This shouldn't happen!"); } @@ -87,17 +85,15 @@ impl UI { loop { print!("{}", prompt); io::stdout().flush().unwrap(); - + let mut input = String::new(); match io::stdin().read_line(&mut input) { - Ok(_) => { - match input.trim().parse::() { - Ok(num) => return num, - Err(_) => { - println!("āŒ Please enter a valid number!"); - } + Ok(_) => match input.trim().parse::() { + Ok(num) => return num, + Err(_) => { + println!("āŒ Please enter a valid number!"); } - } + }, Err(_) => { println!("āŒ Error reading input! Please try again."); } @@ -139,16 +135,14 @@ impl UI { loop { print!("\nšŸ”„ Would you like to play again? (y/n): "); io::stdout().flush().unwrap(); - + let mut input = String::new(); match io::stdin().read_line(&mut input) { - Ok(_) => { - match input.trim().to_lowercase().as_str() { - "y" | "yes" => return true, - "n" | "no" => return false, - _ => println!("āŒ Please enter 'y' for yes or 'n' for no."), - } - } + Ok(_) => match input.trim().to_lowercase().as_str() { + "y" | "yes" => return true, + "n" | "no" => return false, + _ => println!("āŒ Please enter 'y' for yes or 'n' for no."), + }, Err(_) => { println!("āŒ Error reading input! Please try again."); } @@ -164,52 +158,52 @@ mod tests { #[test] fn test_ui_game_flow_integration() { let mut game = Game::new(); - + assert_eq!(*game.get_state(), GameState::InProgress); assert_eq!(game.get_current_player(), Player::X); - + assert!(game.make_move(1)); // X assert_eq!(game.get_current_player(), Player::O); - + assert!(game.make_move(2)); // O assert_eq!(game.get_current_player(), Player::X); - + assert_eq!(*game.get_state(), GameState::InProgress); } - + #[test] fn test_ui_handles_invalid_moves() { let mut game = Game::new(); - + assert!(game.make_move(5)); - - assert!(!game.make_move(5)); // Already occupied - assert!(!game.make_move(0)); // Invalid position + + assert!(!game.make_move(5)); // Already occupied + assert!(!game.make_move(0)); // Invalid position assert!(!game.make_move(10)); // Invalid position - + assert_eq!(game.get_current_player(), Player::O); } - + #[test] fn test_ui_game_completion() { let mut game = Game::new(); - + game.make_move(1); // X game.make_move(4); // O game.make_move(2); // X game.make_move(5); // O game.make_move(3); // X wins - + assert_eq!(*game.get_state(), GameState::Won(Player::X)); - + assert!(!game.make_move(6)); assert!(!game.make_move(7)); } - + #[test] fn test_ui_draw_scenario() { let mut game = Game::new(); - + game.make_move(1); // X game.make_move(2); // O game.make_move(3); // X @@ -219,8 +213,8 @@ mod tests { game.make_move(6); // X game.make_move(7); // O game.make_move(8); // X - + assert_eq!(*game.get_state(), GameState::Draw); assert!(!game.make_move(1)); } -} \ No newline at end of file +}