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/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/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 diff --git a/topics/tic-tac-toe/src/ai.rs b/topics/tic-tac-toe/src/ai.rs new file mode 100644 index 0000000..2d4bf32 --- /dev/null +++ b/topics/tic-tac-toe/src/ai.rs @@ -0,0 +1,228 @@ +use crate::board::{Cell, Player}; +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 + } +} diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs new file mode 100644 index 0000000..addd129 --- /dev/null +++ b/topics/tic-tac-toe/src/board.rs @@ -0,0 +1,225 @@ +#[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 + } + } + + /// 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_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.place_move(1, Player::O)); + assert_eq!(board.get_cell(1), Some(Cell::Occupied(Player::O))); + + 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_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)); + } +} diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs new file mode 100644 index 0000000..3f3b231 --- /dev/null +++ b/topics/tic-tac-toe/src/game.rs @@ -0,0 +1,362 @@ +use crate::board::{Board, Cell, Player}; + +/// 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; + } + + 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 + } + + /// Gets all available moves + pub fn get_available_moves(&self) -> Vec { + if self.state != GameState::InProgress { + Vec::new() + } else { + self.board.get_empty_positions() + } + } + + /// 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 { + 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 { + 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); + + // 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); + + 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_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.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 + 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)); + + 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() { + 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)); + + 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)); + + 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() { + 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)); + + 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(); + + 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); + } + + #[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 + 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(); + + // 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()); + } +} diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..68278a9 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,11 @@ +mod ai; +mod board; +mod game; +mod ui; + +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 new file mode 100644 index 0000000..9a1589d --- /dev/null +++ b/topics/tic-tac-toe/src/ui.rs @@ -0,0 +1,220 @@ +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 +pub struct UI; + +impl UI { + /// Starts the interactive game loop + pub fn play_game() { + 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; + } + } + } + + /// 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 + Self::handle_human_turn(game); + } + Player::O => { + // AI's turn + Self::handle_ai_turn(game, ai); + } + } + } + + 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("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 { + println!("❌ Invalid position! Please enter a number between 1 and 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) { + println!("❌ AI error: Failed to make move. This shouldn't happen!"); + } + } else { + println!("❌ AI error: No valid moves available. This shouldn't happen!"); + } + } + + /// 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() { + 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(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 + 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.get_state(), GameState::Draw); + assert!(!game.make_move(1)); + } +}