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..daac724 --- /dev/null +++ b/topics/tic-tac-toe/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2021" +description = "A command-line tic-tac-toe game with AI opponent using minimax algorithm" + +[[bin]] +name = "tic-tac-toe" +path = "src/main.rs" + +[dependencies] \ No newline at end of file diff --git a/topics/tic-tac-toe/docs/architecture.md b/topics/tic-tac-toe/docs/architecture.md new file mode 100644 index 0000000..7100e90 --- /dev/null +++ b/topics/tic-tac-toe/docs/architecture.md @@ -0,0 +1,39 @@ +# Tic-Tac-Toe AI Agent - Architecture + +## Project Definition + +A command-line tic-tac-toe game where a human player competes against an AI opponent using the minimax algorithm. The AI plays optimally and cannot be beaten. + +### Goals + +- Implement optimal AI using minimax algorithm +- Provide clean command-line interface +- Maintain modular, testable code structure + +## Components and Modules + +### Board Module (`src/board.rs`) + +Manages game state using a 3x3 grid. Handles move validation, winner detection, and board display. + +### AI Module (`src/ai.rs`) + +Implements minimax algorithm for optimal move selection. + +### Game Module (`src/game.rs`) + +Coordinates gameplay flow, manages turns, and handles player interactions. + +### Main Module (`src/main.rs`) + +Provides command-line interface and user input handling. + +## Usage + +Build and run: + +```bash +cargo run +``` + +Enter moves as coordinates (0-2): `1 2` for row 1, column 2. Type `quit` to exit. diff --git a/topics/tic-tac-toe/src/ai.rs b/topics/tic-tac-toe/src/ai.rs new file mode 100644 index 0000000..b0fbb95 --- /dev/null +++ b/topics/tic-tac-toe/src/ai.rs @@ -0,0 +1,270 @@ +//! AI module - Minimax algorithm implementation + +use crate::board::{Board, Cell}; + +/// AI agent that uses minimax algorithm to determine optimal moves +pub struct AiAgent; + +impl AiAgent { + /// Creates a new AI agent + pub fn new() -> Self { + Self + } + + /// Returns the best move for the AI player using minimax algorithm with alpha-beta pruning + /// Returns None if no moves are available (game is over) + pub fn get_best_move(&self, board: &Board) -> Option<(usize, usize)> { + let empty_positions = board.empty_positions(); + + if empty_positions.is_empty() { + return None; + } + + let mut best_score = i32::MIN; + let mut best_moves = Vec::new(); + + for (row, col) in empty_positions { + let mut board_copy = board.clone(); + board_copy.set(row, col, Cell::O); + + let score = Self::minimax_alpha_beta(&board_copy, 0, false, i32::MIN, i32::MAX); + + if score > best_score { + best_score = score; + best_moves.clear(); + best_moves.push((row, col)); + } else if score == best_score { + best_moves.push((row, col)); + } + } + + // If multiple moves have the same score, prioritize strategically + Self::select_strategic_move(&best_moves) + } + + /// Select the most strategic move from equally scored positions + /// Priority: center > corners > edges + fn select_strategic_move(moves: &[(usize, usize)]) -> Option<(usize, usize)> { + if moves.is_empty() { + return None; + } + + // Check for center position (1,1) + if moves.contains(&(1, 1)) { + return Some((1, 1)); + } + + // Check for corner positions + let corners = [(0, 0), (0, 2), (2, 0), (2, 2)]; + for corner in corners { + if moves.contains(&corner) { + return Some(corner); + } + } + + // Return any remaining move (edges) + Some(moves[0]) + } + + /// Minimax algorithm with alpha-beta pruning for improved performance + fn minimax_alpha_beta( + board: &Board, + depth: usize, + is_maximizing: bool, + mut alpha: i32, + mut beta: i32, + ) -> i32 { + // Check for terminal states + if let Some(winner) = board.check_winner() { + return match winner { + Cell::O => 100 - depth as i32, // AI wins (prefer shorter paths to victory) + Cell::X => depth as i32 - 100, // Human wins (prefer longer paths to defeat) + Cell::Empty => 0, // Should never happen in practice + }; + } + + // If board is full, it's a draw + if board.is_full() { + return 0; + } + + if is_maximizing { + // AI's turn - maximize score + let mut max_score = i32::MIN; + + for (row, col) in board.empty_positions() { + let mut board_copy = board.clone(); + board_copy.set(row, col, Cell::O); + + let score = Self::minimax_alpha_beta(&board_copy, depth + 1, false, alpha, beta); + max_score = max_score.max(score); + alpha = alpha.max(score); + + // Alpha-beta pruning + if beta <= alpha { + break; + } + } + + max_score + } else { + // Human's turn - minimize score + let mut min_score = i32::MAX; + + for (row, col) in board.empty_positions() { + let mut board_copy = board.clone(); + board_copy.set(row, col, Cell::X); + + let score = Self::minimax_alpha_beta(&board_copy, depth + 1, true, alpha, beta); + min_score = min_score.min(score); + beta = beta.min(score); + + // Alpha-beta pruning + if beta <= alpha { + break; + } + } + + min_score + } + } +} + +impl Default for AiAgent { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ai_agent_creation() { + let ai = AiAgent::new(); + let board = Board::new(); + assert!(ai.get_best_move(&board).is_some()); + } + + #[test] + fn test_ai_blocks_winning_move() { + let mut board = Board::new(); + board.set(0, 0, Cell::X); + board.set(0, 1, Cell::X); + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + assert_eq!(best_move, Some((0, 2))); + } + + #[test] + fn test_ai_takes_winning_move() { + let mut board = Board::new(); + board.set(1, 1, Cell::O); + board.set(0, 0, Cell::O); + board.set(2, 1, Cell::X); + board.set(1, 0, Cell::X); + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + assert_eq!(best_move, Some((2, 2))); + } + + #[test] + fn test_ai_prefers_center_on_empty_board() { + let board = Board::new(); + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + assert_eq!(best_move, Some((1, 1))); + } + + #[test] + fn test_ai_no_moves_available() { + let mut board = Board::new(); + // Fill the entire board + board.set(0, 0, Cell::X); + board.set(0, 1, Cell::O); + board.set(0, 2, Cell::X); + board.set(1, 0, Cell::O); + board.set(1, 1, Cell::X); + board.set(1, 2, Cell::O); + board.set(2, 0, Cell::X); + board.set(2, 1, Cell::O); + board.set(2, 2, Cell::X); + + let ai = AiAgent::new(); + assert_eq!(ai.get_best_move(&board), None); + } + + #[test] + fn test_strategic_move_selection() { + // Test center preference + let moves = vec![(0, 1), (1, 1), (2, 1)]; + assert_eq!(AiAgent::select_strategic_move(&moves), Some((1, 1))); + + // Test corner preference when no center + let moves = vec![(0, 1), (0, 0), (2, 1)]; + assert_eq!(AiAgent::select_strategic_move(&moves), Some((0, 0))); + + // Test edge selection when no center or corners + let moves = vec![(0, 1), (1, 0), (2, 1)]; + assert_eq!(AiAgent::select_strategic_move(&moves), Some((0, 1))); + } + + #[test] + fn test_ai_fork_blocking() { + let mut board = Board::new(); + // Set up a fork scenario where human has two ways to win + // X in corners creates a fork + board.set(0, 0, Cell::X); // Top-left corner + board.set(2, 2, Cell::X); // Bottom-right corner + board.set(1, 1, Cell::O); // AI has center + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + + // AI should block one of the winning paths + // Valid blocking moves: (0,2), (2,0), (0,1), (1,0), (1,2), (2,1) + let blocking_moves = vec![(0, 2), (2, 0), (0, 1), (1, 0), (1, 2), (2, 1)]; + assert!(blocking_moves.contains(&best_move.unwrap())); + } + + #[test] + fn test_ai_priorities_winning_over_blocking() { + let mut board = Board::new(); + // Set up scenario where AI can win OR block human win + board.set(0, 0, Cell::O); // AI + board.set(0, 1, Cell::O); // AI (can win at 0,2) + board.set(1, 0, Cell::X); // Human + board.set(1, 1, Cell::X); // Human (can win at 1,2) + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + + // AI should prioritize winning over blocking + assert_eq!(best_move, Some((0, 2))); + } + + #[test] + fn test_ai_corner_response() { + let mut board = Board::new(); + // If human takes a corner, AI should take center + board.set(0, 0, Cell::X); + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + assert_eq!(best_move, Some((1, 1))); + + // If center is taken, AI should take opposite corner + let mut board = Board::new(); + board.set(0, 0, Cell::X); // Human takes corner + board.set(1, 1, Cell::X); // Human takes center + + let ai = AiAgent::new(); + let best_move = ai.get_best_move(&board); + // Should take opposite corner (2,2) or another strategic position + let strategic_moves = vec![(2, 2), (0, 2), (2, 0)]; + assert!(strategic_moves.contains(&best_move.unwrap())); + } +} diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs new file mode 100644 index 0000000..dcaac40 --- /dev/null +++ b/topics/tic-tac-toe/src/board.rs @@ -0,0 +1,270 @@ +//! Board module - Game state representation + +use std::fmt; + +/// Board size constant +const BOARD_SIZE: usize = 3; + +/// Represents a cell on the tic-tac-toe board +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cell { + Empty, + X, + O, +} + +impl fmt::Display for Cell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Cell::Empty => write!(f, " "), + Cell::X => write!(f, "X"), + Cell::O => write!(f, "O"), + } + } +} + +/// Represents the 3x3 tic-tac-toe board +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Board { + cells: [[Cell; BOARD_SIZE]; BOARD_SIZE], +} + +impl Board { + /// Creates a new empty board + pub fn new() -> Self { + Self { + cells: [[Cell::Empty; BOARD_SIZE]; BOARD_SIZE], + } + } + + /// Gets the cell at the specified position + pub fn get(&self, row: usize, col: usize) -> Option { + if row < BOARD_SIZE && col < BOARD_SIZE { + Some(self.cells[row][col]) + } else { + None + } + } + + /// Sets the cell at the specified position + /// Returns true if the move was valid (cell was empty), false otherwise + pub fn set(&mut self, row: usize, col: usize, cell: Cell) -> bool { + if row < BOARD_SIZE && col < BOARD_SIZE && self.cells[row][col] == Cell::Empty { + self.cells[row][col] = cell; + true + } else { + false + } + } + + /// Checks if the specified position is empty + pub fn is_empty(&self, row: usize, col: usize) -> bool { + self.get(row, col) == Some(Cell::Empty) + } + + /// Returns true if the board is full + pub fn is_full(&self) -> bool { + for row in 0..BOARD_SIZE { + for col in 0..BOARD_SIZE { + if self.cells[row][col] == Cell::Empty { + return false; + } + } + } + true + } + + /// Gets all empty positions on the board + pub fn empty_positions(&self) -> Vec<(usize, usize)> { + let mut positions = Vec::new(); + for row in 0..BOARD_SIZE { + for col in 0..BOARD_SIZE { + if self.cells[row][col] == Cell::Empty { + positions.push((row, col)); + } + } + } + positions + } + + /// Checks if there's a winner and returns the winning cell type + pub fn check_winner(&self) -> Option { + // Check rows + for row in 0..BOARD_SIZE { + if self.cells[row][0] != Cell::Empty + && self.cells[row][0] == self.cells[row][1] + && self.cells[row][1] == self.cells[row][2] + { + return Some(self.cells[row][0]); + } + } + + // Check columns + for col in 0..BOARD_SIZE { + if self.cells[0][col] != Cell::Empty + && self.cells[0][col] == self.cells[1][col] + && self.cells[1][col] == self.cells[2][col] + { + return Some(self.cells[0][col]); + } + } + + // Check main diagonal (top-left to bottom-right) + if self.cells[0][0] != Cell::Empty + && self.cells[0][0] == self.cells[1][1] + && self.cells[1][1] == self.cells[2][2] + { + return Some(self.cells[0][0]); + } + + // Check anti-diagonal (top-right to bottom-left) + if self.cells[0][2] != Cell::Empty + && self.cells[0][2] == self.cells[1][1] + && self.cells[1][1] == self.cells[2][0] + { + return Some(self.cells[0][2]); + } + + None + } + + /// Returns true 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.is_full() + } +} + +impl Default for Board { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for Board { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, " 0 1 2")?; + for row in 0..BOARD_SIZE { + write!(f, "{} ", row)?; + for col in 0..BOARD_SIZE { + write!(f, "{}", self.cells[row][col])?; + if col < BOARD_SIZE - 1 { + write!(f, " | ")?; + } + } + writeln!(f)?; + if row < BOARD_SIZE - 1 { + writeln!(f, " ---------")?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_board() { + let board = Board::new(); + assert!(board.is_empty(0, 0)); + assert!(board.is_empty(2, 2)); + assert!(!board.is_full()); + assert!(board.check_winner().is_none()); + } + + #[test] + fn test_set_and_get() { + let mut board = Board::new(); + assert!(board.set(1, 1, Cell::X)); + assert_eq!(board.get(1, 1), Some(Cell::X)); + assert!(!board.set(1, 1, Cell::O)); // Can't overwrite + } + + #[test] + fn test_winner_detection() { + let mut board = Board::new(); + + // Set up a winning row + board.set(0, 0, Cell::X); + board.set(0, 1, Cell::X); + board.set(0, 2, Cell::X); + + assert_eq!(board.check_winner(), Some(Cell::X)); + } + + #[test] + fn test_all_winning_conditions() { + // Test all 8 possible winning combinations + + // Test all rows + for row in 0..BOARD_SIZE { + let mut board = Board::new(); + for col in 0..BOARD_SIZE { + board.set(row, col, Cell::O); + } + assert_eq!( + board.check_winner(), + Some(Cell::O), + "Row {} should be a win", + row + ); + } + + // Test all columns + for col in 0..BOARD_SIZE { + let mut board = Board::new(); + for row in 0..BOARD_SIZE { + board.set(row, col, Cell::X); + } + assert_eq!( + board.check_winner(), + Some(Cell::X), + "Column {} should be a win", + col + ); + } + + // Test main diagonal (top-left to bottom-right) + let mut board = Board::new(); + for i in 0..BOARD_SIZE { + board.set(i, i, Cell::O); + } + assert_eq!( + board.check_winner(), + Some(Cell::O), + "Main diagonal should be a win" + ); + + // Test anti-diagonal (top-right to bottom-left) + let mut board = Board::new(); + for i in 0..BOARD_SIZE { + board.set(i, BOARD_SIZE - 1 - i, Cell::X); + } + assert_eq!( + board.check_winner(), + Some(Cell::X), + "Anti-diagonal should be a win" + ); + } + + #[test] + fn test_draw_detection() { + let mut board = Board::new(); + + // Create a draw scenario: X O X / O X O / O X O + board.set(0, 0, Cell::X); + board.set(0, 1, Cell::O); + board.set(0, 2, Cell::X); + board.set(1, 0, Cell::O); + board.set(1, 1, Cell::X); + board.set(1, 2, Cell::O); + board.set(2, 0, Cell::O); + board.set(2, 1, Cell::X); + board.set(2, 2, Cell::O); + + assert!(board.is_full()); + assert!(board.check_winner().is_none()); + assert!(board.is_game_over()); + } +} diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs new file mode 100644 index 0000000..d9abe3e --- /dev/null +++ b/topics/tic-tac-toe/src/game.rs @@ -0,0 +1,320 @@ +//! Game module - Main game logic + +use crate::ai::AiAgent; +use crate::board::{Board, Cell}; +use std::fmt; + +/// Board size constant +const BOARD_SIZE: usize = 3; + +/// Represents the two players in the game +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Player { + Human, + Ai, +} + +/// Represents the possible game outcomes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GameResult { + HumanWin, + AiWin, + Draw, +} + +/// Represents errors that can occur during gameplay +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GameError { + InvalidPosition, + PositionOccupied, + GameOver, + WrongPlayer, +} + +impl fmt::Display for GameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GameError::InvalidPosition => write!(f, "Invalid position (must be 0-2)"), + GameError::PositionOccupied => write!(f, "Position is already occupied"), + GameError::GameOver => write!(f, "Game is already over"), + GameError::WrongPlayer => write!(f, "Not your turn"), + } + } +} + +impl std::error::Error for GameError {} + +/// Main game controller that manages the tic-tac-toe game +pub struct Game { + board: Board, + current_player: Player, + ai_agent: AiAgent, +} + +impl Game { + /// Creates a new game with the human player going first + pub fn new() -> Self { + Self { + board: Board::new(), + current_player: Player::Human, + ai_agent: AiAgent::new(), + } + } + + /// Returns the current player + pub fn current_player(&self) -> Player { + self.current_player + } + + /// Returns a reference to the current board + pub fn board(&self) -> &Board { + &self.board + } + + /// Displays the current board state + pub fn display_board(&self) { + println!("{}", self.board); + } + + /// Makes a move for the human player + pub fn make_human_move(&mut self, row: usize, col: usize) -> Result<(), GameError> { + // Check if game is over + if self.board.is_game_over() { + return Err(GameError::GameOver); + } + + // Check if it's the human player's turn + if self.current_player != Player::Human { + return Err(GameError::WrongPlayer); + } + + // Validate position + if row >= BOARD_SIZE || col >= BOARD_SIZE { + return Err(GameError::InvalidPosition); + } + + // Check if position is empty + if !self.board.is_empty(row, col) { + return Err(GameError::PositionOccupied); + } + + // Make the move + self.board.set(row, col, Cell::X); + + // Switch to AI player if game is not over + if !self.board.is_game_over() { + self.current_player = Player::Ai; + } + + Ok(()) + } + + /// Makes a move for the AI player + pub fn make_ai_move(&mut self) -> Result<(), GameError> { + // Check if game is over + if self.board.is_game_over() { + return Err(GameError::GameOver); + } + + // Check if it's the AI player's turn + if self.current_player != Player::Ai { + return Err(GameError::WrongPlayer); + } + + // Get the best move from the AI + if let Some((row, col)) = self.ai_agent.get_best_move(&self.board) { + self.board.set(row, col, Cell::O); + + // Switch to human player if game is not over + if !self.board.is_game_over() { + self.current_player = Player::Human; + } + + Ok(()) + } else { + // This should not happen if the game logic is correct + Err(GameError::GameOver) + } + } + + /// Checks if the game is over and returns the result + pub fn check_game_over(&self) -> Option { + if let Some(winner) = self.board.check_winner() { + match winner { + Cell::X => Some(GameResult::HumanWin), + Cell::O => Some(GameResult::AiWin), + Cell::Empty => None, // This should never happen + } + } else if self.board.is_full() { + Some(GameResult::Draw) + } else { + None + } + } + + /// Resets the game to initial state + pub fn reset(&mut self) { + self.board = Board::new(); + self.current_player = Player::Human; + } +} + +impl Default for Game { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_game() { + let game = Game::new(); + assert_eq!(game.current_player(), Player::Human); + assert!(game.check_game_over().is_none()); + } + + #[test] + fn test_human_move() { + let mut game = Game::new(); + assert!(game.make_human_move(1, 1).is_ok()); + assert_eq!(game.current_player(), Player::Ai); + assert_eq!(game.board().get(1, 1), Some(Cell::X)); + } + + #[test] + fn test_invalid_moves() { + let mut game = Game::new(); + + // Test invalid position + assert_eq!(game.make_human_move(3, 3), Err(GameError::InvalidPosition)); + + // Test occupied position + game.make_human_move(0, 0).unwrap(); + game.make_ai_move().unwrap(); // Switch turns + assert_eq!(game.make_human_move(0, 0), Err(GameError::PositionOccupied)); + } + + #[test] + fn test_wrong_player_errors() { + let mut game = Game::new(); + + // Test human trying to move when it's AI's turn + game.make_human_move(1, 1).unwrap(); // Human moves first + assert_eq!(game.current_player(), Player::Ai); + assert_eq!(game.make_human_move(0, 0), Err(GameError::WrongPlayer)); + + // Test AI trying to move when it's human's turn + game.make_ai_move().unwrap(); // AI moves + assert_eq!(game.current_player(), Player::Human); + assert_eq!(game.make_ai_move(), Err(GameError::WrongPlayer)); + } + + #[test] + fn test_game_over_scenarios() { + // Manually set up a winning condition by directly manipulating the board + // This bypasses the AI logic and ensures we can test game over behavior + let mut board = Board::new(); + board.set(0, 0, Cell::X); + board.set(0, 1, Cell::X); + board.set(0, 2, Cell::X); // X wins + + // Create a new game with this winning board + let mut winning_game = Game::new(); + winning_game.board = board; + + // Verify game is over + assert_eq!(winning_game.check_game_over(), Some(GameResult::HumanWin)); + + // Try to make moves after game is over + assert_eq!(winning_game.make_human_move(1, 1), Err(GameError::GameOver)); + assert_eq!(winning_game.make_ai_move(), Err(GameError::GameOver)); + } + + #[test] + fn test_game_reset() { + let mut game = Game::new(); + game.make_human_move(1, 1).unwrap(); + game.reset(); + + assert_eq!(game.current_player(), Player::Human); + assert!(game.board().is_empty(1, 1)); + } + + #[test] + fn test_complete_game_flow_ai_win() { + let mut game = Game::new(); + + // Simulate a game where AI wins + // This specific sequence should result in AI victory + game.make_human_move(0, 0).unwrap(); // X at (0,0) + assert_eq!(game.current_player(), Player::Ai); + + game.make_ai_move().unwrap(); // AI takes center or strategic position + assert_eq!(game.current_player(), Player::Human); + + game.make_human_move(0, 1).unwrap(); // X at (0,1) + game.make_ai_move().unwrap(); // AI blocks or creates threat + + game.make_human_move(1, 0).unwrap(); // X at (1,0) + game.make_ai_move().unwrap(); // AI should win or block + + // Continue until game ends + while game.check_game_over().is_none() { + if game.current_player() == Player::Human { + // Find any valid move for human + let empty_positions = game.board().empty_positions(); + if let Some((row, col)) = empty_positions.first() { + let _ = game.make_human_move(*row, *col); + } + } else { + let _ = game.make_ai_move(); + } + } + + // Game should end in AI win or draw (AI should never lose) + let result = game.check_game_over().unwrap(); + assert!(result == GameResult::AiWin || result == GameResult::Draw); + } + + #[test] + fn test_draw_game_flow() { + let mut game = Game::new(); + + // Test that AI can handle full game scenarios properly + // Let the AI and human alternate moves until game ends + let mut move_count = 0; + const MAX_MOVES: i32 = 9; // Maximum possible moves in tic-tac-toe + + while game.check_game_over().is_none() && move_count < MAX_MOVES { + if game.current_player() == Player::Human { + // Find first available empty position + let empty_positions = game.board().empty_positions(); + if let Some((row, col)) = empty_positions.first() { + if game.make_human_move(*row, *col).is_ok() { + move_count += 1; + } + } else { + break; // No more moves available + } + } else { + if game.make_ai_move().is_ok() { + move_count += 1; + } else { + break; // AI can't move + } + } + } + + // Game should end with either a result or a full board + let result = game.check_game_over(); + assert!(result.is_some() || game.board().is_full()); + + // With optimal AI play, human should never win + if let Some(game_result) = result { + assert!(game_result != GameResult::HumanWin); + } + } +} diff --git a/topics/tic-tac-toe/src/lib.rs b/topics/tic-tac-toe/src/lib.rs new file mode 100644 index 0000000..70abc0d --- /dev/null +++ b/topics/tic-tac-toe/src/lib.rs @@ -0,0 +1,9 @@ +//! Tic-Tac-Toe Game Library + +pub mod ai; +pub mod board; +pub mod game; + +pub use ai::AiAgent; +pub use board::{Board, Cell}; +pub use game::{Game, GameError, GameResult, Player}; diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..5a6b486 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,100 @@ +//! Tic-Tac-Toe Game with AI + +use std::io::{self, Write}; +use tic_tac_toe::Game; + +/// Board size constant +const BOARD_SIZE: usize = 3; + +fn main() { + println!("🎮 Welcome to Tic-Tac-Toe!"); + println!("You are playing as 'X' against the AI 'O'"); + println!("Enter your moves as coordinates (row, col) from 0-2"); + println!("Example: '1 2' places your mark at row 1, column 2"); + println!(); + + let mut game = Game::new(); + + loop { + // Display the current board + game.display_board(); + + match game.current_player() { + tic_tac_toe::Player::Human => match get_human_move() { + Some((row, col)) => match game.make_human_move(row, col) { + Ok(_) => {} + Err(e) => { + println!("❌ Invalid move: {}", e); + continue; + } + }, + None => { + println!("👋 Thanks for playing!"); + return; + } + }, + tic_tac_toe::Player::Ai => { + println!("🤖 AI is thinking..."); + match game.make_ai_move() { + Ok(_) => println!("✅ AI made its move!"), + Err(e) => { + println!("❌ AI error: {}", e); + break; + } + } + } + } + + if let Some(result) = game.check_game_over() { + game.display_board(); + match result { + tic_tac_toe::GameResult::HumanWin => println!("🎉 Congratulations! You won!"), + tic_tac_toe::GameResult::AiWin => println!("🤖 AI wins! Better luck next time!"), + tic_tac_toe::GameResult::Draw => println!("🤝 It's a draw! Good game!"), + } + break; + } + } +} + +/// Get a move from the human player +fn get_human_move() -> Option<(usize, usize)> { + loop { + print!("Enter your move (row col) or 'quit' to exit: "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + match io::stdin().read_line(&mut input) { + Ok(_) => { + let input = input.trim(); + + if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("q") { + return None; + } + + let parts: Vec<&str> = input.split_whitespace().collect(); + if parts.len() != 2 { + println!("❌ Please enter two numbers separated by a space (e.g., '1 2')"); + continue; + } + + match (parts[0].parse::(), parts[1].parse::()) { + (Ok(row), Ok(col)) => { + if row < BOARD_SIZE && col < BOARD_SIZE { + return Some((row, col)); + } else { + println!("❌ Coordinates must be between 0 and {}", BOARD_SIZE - 1); + } + } + _ => { + println!("❌ Please enter valid numbers"); + } + } + } + Err(_) => { + println!("❌ Error reading input"); + continue; + } + } + } +}