From 47568124a3095e30131f2a0d0f0a876143cb8e8c Mon Sep 17 00:00:00 2001 From: "auriane.pusel" Date: Sun, 19 Oct 2025 15:13:48 +0200 Subject: [PATCH 1/4] feat : init project and add basic types and board structure Signed-off-by: auriane.pusel --- topics/tic-tac-toe/Cargo.lock | 7 +++ topics/tic-tac-toe/Cargo.toml | 7 +++ topics/tic-tac-toe/src/board.rs | 83 +++++++++++++++++++++++++++++++++ topics/tic-tac-toe/src/main.rs | 6 +++ topics/tic-tac-toe/src/types.rs | 50 ++++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 topics/tic-tac-toe/Cargo.lock create mode 100644 topics/tic-tac-toe/Cargo.toml create mode 100644 topics/tic-tac-toe/src/board.rs create mode 100644 topics/tic-tac-toe/src/main.rs create mode 100644 topics/tic-tac-toe/src/types.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/Cargo.toml b/topics/tic-tac-toe/Cargo.toml new file mode 100644 index 0000000..29e0885 --- /dev/null +++ b/topics/tic-tac-toe/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2024" +authors = ["Auriane"] + +[dependencies] diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs new file mode 100644 index 0000000..166ce11 --- /dev/null +++ b/topics/tic-tac-toe/src/board.rs @@ -0,0 +1,83 @@ +use crate::types::{Cell, Player}; + +/// Represents the game board (3x3 grid) +#[derive(Debug, Clone)] +pub struct Board { + /// Internal representation as a 1D array of 9 cells + cells: [Cell; 9], +} + +impl Board { + /// Creates a new empty board + pub fn new() -> Self { + Board { + cells: [Cell::Empty; 9], + } + } + + /// Returns the cell at the given position (0-8) + pub fn get(&self, position: usize) -> Option { + self.cells.get(position).copied() + } + + /// Places a player's mark at the given position + /// Returns true if the move was successful, false otherwise + pub fn make_move(&mut self, position: usize, player: Player) -> bool { + if position >= 9 { + return false; + } + + if self.cells[position].is_empty() { + self.cells[position] = Cell::Occupied(player); + true + } else { + false + } + } + + /// Returns a list of all available moves (empty cell positions) + pub fn available_moves(&self) -> Vec { + self.cells + .iter() + .enumerate() + .filter(|(_, cell)| cell.is_empty()) + .map(|(idx, _)| idx) + .collect() + } + + /// Returns true if the board is full (no available moves) + pub fn is_full(&self) -> bool { + self.cells.iter().all(|cell| !cell.is_empty()) + } + + /// Display the board + pub fn display(&self) { + println!("\n"); + for row in 0..3 { + print!(" "); + for col in 0..3 { + let idx = row * 3 + col; + print!(" {} ", self.cells[idx].symbol()); + if col < 2 { + print!("|"); + } + } + println!(); + if row < 2 { + println!(" -----------"); + } + } + println!("\n"); + } + + /// Returns the internal cells array (for testing purposes) + pub fn cells(&self) -> &[Cell; 9] { + &self.cells + } +} + +impl Default for Board { + fn default() -> Self { + Self::new() + } +} diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..eb1d8d8 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,6 @@ +mod types; +mod board; + +fn main() { + println!("Tic-Tac-Toe game - Coming soon!"); +} diff --git a/topics/tic-tac-toe/src/types.rs b/topics/tic-tac-toe/src/types.rs new file mode 100644 index 0000000..90d31a4 --- /dev/null +++ b/topics/tic-tac-toe/src/types.rs @@ -0,0 +1,50 @@ +/// Represents a player in the game +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Player { + /// Human player (X) + Human, + /// AI player (O) + AI, +} + +impl Player { + /// Returns the opposite player + pub fn opponent(&self) -> Player { + match self { + Player::Human => Player::AI, + Player::AI => Player::Human, + } + } + + /// Returns the symbol representing this player + pub fn symbol(&self) -> char { + match self { + Player::Human => 'X', + Player::AI => 'O', + } + } +} + +/// Represents a cell on the board +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cell { + /// Empty cell + Empty, + /// Cell occupied by a player + Occupied(Player), +} + +impl Cell { + /// Returns true if the cell is empty + pub fn is_empty(&self) -> bool { + matches!(self, Cell::Empty) + } + + /// Returns the symbol representing this cell + pub fn symbol(&self) -> char { + match self { + Cell::Empty => ' ', + Cell::Occupied(player) => player.symbol(), + } + } +} From 64c96fed8862abfb5dee6da5912d824155862af4 Mon Sep 17 00:00:00 2001 From: "auriane.pusel" Date: Sun, 19 Oct 2025 16:12:50 +0200 Subject: [PATCH 2/4] feat: implement Minimax AI algorithm and game logic Signed-off-by: auriane.pusel --- topics/tic-tac-toe/src/ai.rs | 175 +++++++++++++++++++++++++ topics/tic-tac-toe/src/game.rs | 230 +++++++++++++++++++++++++++++++++ topics/tic-tac-toe/src/main.rs | 2 + 3 files changed, 407 insertions(+) create mode 100644 topics/tic-tac-toe/src/ai.rs create mode 100644 topics/tic-tac-toe/src/game.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..1b3e7c4 --- /dev/null +++ b/topics/tic-tac-toe/src/ai.rs @@ -0,0 +1,175 @@ +use crate::board::Board; +use crate::game::Game; +use crate::types::Player; + +/// AI player using the Minimax algorithm +pub struct AI { + player: Player, +} + +impl AI { + /// Creates a new AI instance + pub fn new() -> Self { + AI { player: Player::AI } + } + + /// Finds the best move for the AI using the Minimax algorithm + /// Returns the position (0-8) of the best move + pub fn find_best_move(&self, game: &Game) -> Option { + let available_moves = game.available_moves(); + + if available_moves.is_empty() { + return None; + } + + let mut best_score = i32::MIN; + let mut best_move = available_moves[0]; + + // Try each available move and evaluate it + for &position in &available_moves { + let mut game_clone = self.simulate_move(game, position, self.player); + let score = self.minimax(&mut game_clone, 0, false); + + if score > best_score { + best_score = score; + best_move = position; + } + } + + Some(best_move) + } + + /// Minimax algorithm with depth tracking + /// + /// # Arguments + /// * `game` - The current game state + /// * `depth` - Current depth in the game tree + /// * `is_maximizing` - True if maximizing player (AI), false if minimizing (Human) + /// + /// # Returns + /// The score of the board state + fn minimax(&self, game: &mut Game, depth: i32, is_maximizing: bool) -> i32 { + // Terminal state: check if game is over + let score = game.evaluate(); + + // If AI won, return score minus depth (prefer faster wins) + if score == 10 { + return score - depth; + } + + // If Human won, return score plus depth (prefer slower losses) + if score == -10 { + return score + depth; + } + + // Check for draw + let available_moves = game.available_moves(); + if available_moves.is_empty() { + return 0; + } + + if is_maximizing { + // Maximizing player (AI) + let mut best_score = i32::MIN; + + for &position in &available_moves { + let mut game_clone = self.simulate_move(game, position, Player::AI); + let score = self.minimax(&mut game_clone, depth + 1, false); + best_score = best_score.max(score); + } + + best_score + } else { + // Minimizing player (Human) + let mut best_score = i32::MAX; + + for &position in &available_moves { + let mut game_clone = self.simulate_move(game, position, Player::Human); + let score = self.minimax(&mut game_clone, depth + 1, true); + best_score = best_score.min(score); + } + + best_score + } + } + + /// Simulates a move and returns a new game state + fn simulate_move(&self, game: &Game, position: usize, player: Player) -> Game { + // Create a copy of the current game using the board state + let mut new_board = Board::new(); + + // Copy the current board state + for i in 0..9 { + if let Some(crate::types::Cell::Occupied(p)) = game.board().get(i) { + new_board.make_move(i, p); + } + } + + // Make the new move on the copied board + new_board.make_move(position, player); + + // Create a new game with this board state + // We need to use Game::from_board or similar + // For now, let's create a helper in Game + self.create_game_from_board(new_board, player.opponent()) + } + + /// Creates a game state from a board + fn create_game_from_board(&self, board: Board, next_player: Player) -> Game { + Game::from_board(board, next_player) + } +} + +impl Default for AI { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ai_blocks_winning_move() { + let mut game = Game::new(); + let ai = AI::new(); + + // Human has two in a row + game.make_move(0); // Human X at position 0 + game.make_move(3); // AI O at position 3 + game.make_move(1); // Human X at position 1 + + // AI should block position 2 to prevent human win + let best_move = ai.find_best_move(&game); + assert_eq!(best_move, Some(2)); + } + + #[test] + fn test_ai_takes_winning_move() { + let mut game = Game::new(); + let ai = AI::new(); + + // Setup: AI has two in a row + game.make_move(0); // Human X + game.make_move(3); // AI O + game.make_move(1); // Human X + game.make_move(4); // AI O + game.make_move(8); // Human X + + // AI should take position 5 to win + let best_move = ai.find_best_move(&game); + assert_eq!(best_move, Some(5)); + } + + #[test] + fn test_ai_finds_move_on_empty_board() { + let game = Game::new(); + let ai = AI::new(); + + // AI should find a valid move + let best_move = ai.find_best_move(&game); + assert!(best_move.is_some()); + assert!(best_move.unwrap() < 9); + } +} diff --git a/topics/tic-tac-toe/src/game.rs b/topics/tic-tac-toe/src/game.rs new file mode 100644 index 0000000..619c654 --- /dev/null +++ b/topics/tic-tac-toe/src/game.rs @@ -0,0 +1,230 @@ +use crate::board::Board; +use crate::types::{Cell, Player}; + +/// Represents the current state of the game +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GameState { + /// Game is still in progress + InProgress, + /// A player has won + Won(Player), + /// Game ended in a draw + Draw, +} + +/// Represents the game logic and state +pub struct Game { + board: Board, + current_player: Player, + state: GameState, +} + +impl Game { + /// Creates a new game with the human player starting + pub fn new() -> Self { + Game { + board: Board::new(), + current_player: Player::Human, + state: GameState::InProgress, + } + } + + /// Creates a game from an existing board state + pub fn from_board(board: Board, current_player: Player) -> Self { + let mut game = Game { + board, + current_player, + state: GameState::InProgress, + }; + game.update_state(); + game + } + + /// Returns a reference to the current board + pub fn board(&self) -> &Board { + &self.board + } + + /// Returns the current player + pub fn current_player(&self) -> Player { + self.current_player + } + + /// Returns the current game state + pub fn state(&self) -> GameState { + self.state + } + + /// Makes a move at the given position for the current player + /// Returns true if the move was successful, false otherwise + pub fn make_move(&mut self, position: usize) -> bool { + // Check if game is already over + if self.state != GameState::InProgress { + return false; + } + + // Try to make the move + if !self.board.make_move(position, self.current_player) { + return false; + } + + // Update game state + self.update_state(); + + // Switch player if game is still in progress + if self.state == GameState::InProgress { + self.current_player = self.current_player.opponent(); + } + + true + } + + /// Updates the game state by checking for wins or draws + fn update_state(&mut self) { + // Check if current player won + if self.check_winner(self.current_player) { + self.state = GameState::Won(self.current_player); + return; + } + + // Check for draw (board is full and no winner) + if self.board.is_full() { + self.state = GameState::Draw; + } + } + + /// Checks if the given player has won the game + pub fn check_winner(&self, player: Player) -> bool { + let cells = self.board.cells(); + let target = Cell::Occupied(player); + + // Check rows + for row in 0..3 { + if cells[row * 3] == target + && cells[row * 3 + 1] == target + && cells[row * 3 + 2] == target + { + return true; + } + } + + // Check columns + for col in 0..3 { + if cells[col] == target && cells[col + 3] == target && cells[col + 6] == target { + return true; + } + } + + // Check diagonals + // Top-left to bottom-right + if cells[0] == target && cells[4] == target && cells[8] == target { + return true; + } + + // Top-right to bottom-left + if cells[2] == target && cells[4] == target && cells[6] == target { + return true; + } + + false + } + + /// Returns a list of available moves + pub fn available_moves(&self) -> Vec { + self.board.available_moves() + } + + /// Evaluates the current board state for the minimax algorithm + /// Returns: +10 for AI win, -10 for Human win, 0 for draw or in progress + pub fn evaluate(&self) -> i32 { + if self.check_winner(Player::AI) { + 10 + } else if self.check_winner(Player::Human) { + -10 + } else { + 0 + } + } +} + +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.state(), GameState::InProgress); + assert_eq!(game.current_player(), Player::Human); + } + + #[test] + fn test_horizontal_win() { + let mut game = Game::new(); + // Human wins with top row + game.make_move(0); // Human X + game.make_move(3); // AI O + game.make_move(1); // Human X + game.make_move(4); // AI O + game.make_move(2); // Human X - wins! + + assert_eq!(game.state(), GameState::Won(Player::Human)); + } + + #[test] + fn test_vertical_win() { + let mut game = Game::new(); + // AI wins with left column + game.make_move(1); // Human X + game.make_move(0); // AI O + game.make_move(2); // Human X + game.make_move(3); // AI O + game.make_move(4); // Human X + game.make_move(6); // AI O - wins! + + assert_eq!(game.state(), GameState::Won(Player::AI)); + } + + #[test] + fn test_diagonal_win() { + let mut game = Game::new(); + // Human wins with diagonal + game.make_move(0); // Human X + game.make_move(1); // AI O + game.make_move(4); // Human X + game.make_move(2); // AI O + game.make_move(8); // Human X - wins! + + assert_eq!(game.state(), GameState::Won(Player::Human)); + } + + #[test] + fn test_draw() { + let mut game = Game::new(); + // Create a draw scenario + game.make_move(0); // Human X + game.make_move(1); // AI O + game.make_move(2); // Human X + game.make_move(4); // AI O + game.make_move(3); // Human X + game.make_move(5); // AI O + game.make_move(7); // Human X + game.make_move(6); // AI O + game.make_move(8); // Human X + + assert_eq!(game.state(), GameState::Draw); + } + + #[test] + fn test_invalid_move() { + let mut game = Game::new(); + game.make_move(0); // Human X + assert!(!game.make_move(0)); // Try to play same position + } +} diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index eb1d8d8..fa4402f 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,5 +1,7 @@ mod types; mod board; +mod game; +mod ai; fn main() { println!("Tic-Tac-Toe game - Coming soon!"); From 1c1bc2b31d4c2610f959ba3ef917f5253045ec59 Mon Sep 17 00:00:00 2001 From: "auriane.pusel" Date: Sun, 19 Oct 2025 17:02:23 +0200 Subject: [PATCH 3/4] feat : add interactive cli Signed-off-by: auriane.pusel --- topics/tic-tac-toe/src/ai.rs | 34 +++++----- topics/tic-tac-toe/src/board.rs | 2 +- topics/tic-tac-toe/src/main.rs | 111 +++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 21 deletions(-) diff --git a/topics/tic-tac-toe/src/ai.rs b/topics/tic-tac-toe/src/ai.rs index 1b3e7c4..48d7a2e 100644 --- a/topics/tic-tac-toe/src/ai.rs +++ b/topics/tic-tac-toe/src/ai.rs @@ -17,7 +17,7 @@ impl AI { /// Returns the position (0-8) of the best move pub fn find_best_move(&self, game: &Game) -> Option { let available_moves = game.available_moves(); - + if available_moves.is_empty() { return None; } @@ -40,23 +40,23 @@ impl AI { } /// Minimax algorithm with depth tracking - /// + /// /// # Arguments /// * `game` - The current game state /// * `depth` - Current depth in the game tree /// * `is_maximizing` - True if maximizing player (AI), false if minimizing (Human) - /// + /// /// # Returns /// The score of the board state fn minimax(&self, game: &mut Game, depth: i32, is_maximizing: bool) -> i32 { // Terminal state: check if game is over let score = game.evaluate(); - + // If AI won, return score minus depth (prefer faster wins) if score == 10 { return score - depth; } - + // If Human won, return score plus depth (prefer slower losses) if score == -10 { return score + depth; @@ -71,24 +71,24 @@ impl AI { if is_maximizing { // Maximizing player (AI) let mut best_score = i32::MIN; - + for &position in &available_moves { let mut game_clone = self.simulate_move(game, position, Player::AI); let score = self.minimax(&mut game_clone, depth + 1, false); best_score = best_score.max(score); } - + best_score } else { // Minimizing player (Human) let mut best_score = i32::MAX; - + for &position in &available_moves { let mut game_clone = self.simulate_move(game, position, Player::Human); let score = self.minimax(&mut game_clone, depth + 1, true); best_score = best_score.min(score); } - + best_score } } @@ -97,17 +97,17 @@ impl AI { fn simulate_move(&self, game: &Game, position: usize, player: Player) -> Game { // Create a copy of the current game using the board state let mut new_board = Board::new(); - + // Copy the current board state for i in 0..9 { if let Some(crate::types::Cell::Occupied(p)) = game.board().get(i) { new_board.make_move(i, p); } } - + // Make the new move on the copied board new_board.make_move(position, player); - + // Create a new game with this board state // We need to use Game::from_board or similar // For now, let's create a helper in Game @@ -134,12 +134,12 @@ mod tests { fn test_ai_blocks_winning_move() { let mut game = Game::new(); let ai = AI::new(); - + // Human has two in a row game.make_move(0); // Human X at position 0 game.make_move(3); // AI O at position 3 game.make_move(1); // Human X at position 1 - + // AI should block position 2 to prevent human win let best_move = ai.find_best_move(&game); assert_eq!(best_move, Some(2)); @@ -149,14 +149,14 @@ mod tests { fn test_ai_takes_winning_move() { let mut game = Game::new(); let ai = AI::new(); - + // Setup: AI has two in a row game.make_move(0); // Human X game.make_move(3); // AI O game.make_move(1); // Human X game.make_move(4); // AI O game.make_move(8); // Human X - + // AI should take position 5 to win let best_move = ai.find_best_move(&game); assert_eq!(best_move, Some(5)); @@ -166,7 +166,7 @@ mod tests { fn test_ai_finds_move_on_empty_board() { let game = Game::new(); let ai = AI::new(); - + // AI should find a valid move let best_move = ai.find_best_move(&game); assert!(best_move.is_some()); diff --git a/topics/tic-tac-toe/src/board.rs b/topics/tic-tac-toe/src/board.rs index 166ce11..23540b6 100644 --- a/topics/tic-tac-toe/src/board.rs +++ b/topics/tic-tac-toe/src/board.rs @@ -26,7 +26,7 @@ impl Board { if position >= 9 { return false; } - + if self.cells[position].is_empty() { self.cells[position] = Cell::Occupied(player); true diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index fa4402f..13cb87f 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,8 +1,113 @@ -mod types; +mod ai; mod board; mod game; -mod ai; +mod types; + +use ai::AI; +use game::{Game, GameState}; +use std::io::{self, Write}; +use types::Player; fn main() { - println!("Tic-Tac-Toe game - Coming soon!"); + println!("================================="); + println!(" Welcome to Tic-Tac-Toe!"); + println!("================================="); + println!(); + println!("You are X, AI is O"); + println!("Enter positions 1-9 as shown:"); + println!(); + display_position_guide(); + println!(); + + let mut game = Game::new(); + let ai = AI::new(); + + loop { + // Display the current board + game.board().display(); + + // Check game state + match game.state() { + GameState::Won(Player::Human) => { + println!("🎉 Congratulations! You won!"); + break; + } + GameState::Won(Player::AI) => { + println!("💻 AI wins! Better luck next time!"); + break; + } + GameState::Draw => { + println!("🤝 It's a draw! Well played!"); + break; + } + GameState::InProgress => { + // Game continues + } + } + + // Current player's turn + if game.current_player() == Player::Human { + // Human turn + println!("Your turn (X)"); + let position = get_human_move(&game); + + if !game.make_move(position) { + println!("❌ Invalid move! Try again."); + continue; + } + } else { + // AI turn + println!("AI is thinking... 🤔"); + + if let Some(position) = ai.find_best_move(&game) { + game.make_move(position); + println!("AI played position {}", position + 1); + } else { + println!("❌ Error: AI couldn't find a move!"); + break; + } + } + } + + println!(); + println!("Thanks for playing!"); +} + +/// Gets a valid move from the human player +fn get_human_move(game: &Game) -> usize { + loop { + print!("Enter position (1-9): "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + + // Try to parse the input + match input.trim().parse::() { + Ok(num) if (1..=9).contains(&num) => { + let position = num - 1; // Convert to 0-indexed + + // Check if position is available + if game.available_moves().contains(&position) { + return position; + } else { + println!("❌ That position is already taken! Try another."); + } + } + _ => { + println!("❌ Invalid input! Please enter a number between 1 and 9."); + } + } + } +} + +/// Displays the position guide (how positions are numbered) +fn display_position_guide() { + println!(" 1 | 2 | 3"); + println!(" -----------"); + println!(" 4 | 5 | 6"); + println!(" -----------"); + println!(" 7 | 8 | 9"); } From 7031bb84d3892f6198160068dc90d4fe8f497e11 Mon Sep 17 00:00:00 2001 From: "auriane.pusel" Date: Sun, 19 Oct 2025 17:18:27 +0200 Subject: [PATCH 4/4] feat: add documentation Signed-off-by: auriane.pusel --- topics/tic-tac-toe/docs/architecture.md | 303 ++++++++++++++++++++++++ topics/tic-tac-toe/src/main.rs | 16 +- 2 files changed, 311 insertions(+), 8 deletions(-) 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..707d177 --- /dev/null +++ b/topics/tic-tac-toe/docs/architecture.md @@ -0,0 +1,303 @@ +# Tic-Tac-Toe AI Agent - Architecture Documentation + +## Project Definition + +### What is it? + +This project is a command-line Tic-Tac-Toe game where a human player competes against an AI opponent. The AI uses the Minimax algorithm to play optimally, ensuring it cannot be beaten - it will either win or draw every game. + +### Goals + +The primary goals of this project are: + +1. **Unbeatable AI**: Implement an AI that plays optimally using the Minimax algorithm with depth-first search +2. **Interactive Gameplay**: Provide a user-friendly command-line interface for humans to play against the AI +3. **Clean Architecture**: Organize the code into well-defined, modular components +4. **Code Quality**: Follow Rust best practices with proper error handling, formatting, and comprehensive testing + +## Components and Modules + +The project is structured into five main modules, each with a specific responsibility: + +### 1. `types.rs` - Core Type Definitions + +**Purpose**: Defines the fundamental types used throughout the application. + +**Key Components**: +- `Player` enum: Represents either Human (X) or AI (O) player + - `opponent()`: Returns the opposing player + - `symbol()`: Returns the character representation ('X' or 'O') +- `Cell` enum: Represents a board cell state (Empty or Occupied by a player) + - `is_empty()`: Checks if the cell is available + - `symbol()`: Returns the display character + +**Rationale**: Separating type definitions provides a single source of truth for core concepts and enables type safety throughout the codebase. + +### 2. `board.rs` - Game Board Representation + +**Purpose**: Manages the 3x3 game board state and provides board manipulation operations. + +**Key Components**: +- `Board` struct: Internally uses a 1D array of 9 cells for efficient storage +- Key methods: + - `new()`: Creates an empty board + - `make_move(position, player)`: Places a player's mark at a position + - `available_moves()`: Returns all empty positions + - `is_full()`: Checks if the board is completely filled + - `display()`: Renders the board to the console + - `get(position)`: Retrieves the cell state at a position + - `cells()`: Provides access to the internal cell array + +**Rationale**: Encapsulating board logic in a dedicated module ensures board operations are consistent and testable. Using a 1D array (index 0-8) simplifies indexing calculations compared to a 2D array. + +### 3. `game.rs` - Game Logic and State Management + +**Purpose**: Implements game rules, win detection, and state transitions. + +**Key Components**: +- `GameState` enum: Tracks the current game status + - `InProgress`: Game is ongoing + - `Won(Player)`: A player has won + - `Draw`: Game ended in a draw +- `Game` struct: Orchestrates the overall game flow +- Key methods: + - `new()`: Initializes a new game with Human starting + - `from_board(board, player)`: Creates a game from an existing board state (used by AI simulations) + - `make_move(position)`: Executes a move and updates game state + - `check_winner(player)`: Checks all win conditions (rows, columns, diagonals) + - `evaluate()`: Returns a score for the current board state (+10 for AI win, -10 for Human win, 0 otherwise) + - `update_state()`: Updates the game state after each move + +**Rationale**: Centralizing game logic separates rules enforcement from board representation and AI logic. The `evaluate()` method provides a bridge between game state and the Minimax algorithm. + +### 4. `ai.rs` - Minimax AI Implementation + +**Purpose**: Implements the AI player using the Minimax algorithm. + +**Key Components**: +- `AI` struct: Represents the AI player +- Key methods: + - `find_best_move(game)`: Finds the optimal move for the current game state + - `minimax(game, depth, is_maximizing)`: Recursive Minimax algorithm implementation + - `simulate_move(game, position, player)`: Creates a hypothetical future game state + - `create_game_from_board(board, player)`: Helper for game state creation + +**Algorithm Details**: +- **Minimax with Depth Optimization**: The algorithm explores all possible future game states recursively + - Maximizing player (AI): Chooses moves that maximize the score + - Minimizing player (Human): Assumes the opponent plays optimally to minimize AI's score + - Depth consideration: Prefers faster wins (score - depth) and slower losses (score + depth) +- **Terminal States**: + - AI wins: +10 + - Human wins: -10 + - Draw: 0 + +**Rationale**: The Minimax algorithm guarantees optimal play by exhaustively searching the game tree. Depth optimization ensures the AI prefers quicker victories. Separating AI logic into its own module allows for potential future AI strategy variations. + +### 5. `main.rs` - User Interface and Game Loop + +**Purpose**: Provides the command-line interface and coordinates the game flow. + +**Key Components**: +- `main()`: Main game loop that alternates between human and AI turns +- `get_human_move(game)`: Handles user input with validation +- `display_position_guide()`: Shows position numbering (1-9) + +**User Experience Features**: +- Clear visual position guide +- Input validation (1-9 range, position availability) +- Informative error messages +- Game result announcements with emojis +- AI thinking indicator + +**Rationale**: Separating the UI from business logic makes the core game engine reusable and testable. The CLI provides an intuitive interface with helpful guidance for users. + +## Module Interaction Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ main.rs │ +│ (User Interface) │ +└────────────┬────────────────────────────┬───────────────┘ + │ │ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ game.rs │ │ ai.rs │ + │ (Game Logic)│◄────────────┤ (AI Player) │ + └──────┬──────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ board.rs │ + │ (Board) │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ types.rs │ + │ (Types) │ + └─────────────┘ +``` + +**Data Flow**: +1. `main.rs` creates a `Game` instance and an `AI` instance +2. For human turns: `main.rs` gets input → validates → calls `game.make_move()` +3. For AI turns: `main.rs` calls `ai.find_best_move()` → AI explores game tree using `game.evaluate()` → returns best position → `main.rs` calls `game.make_move()` +4. `Game` updates the `Board` and checks win conditions +5. `main.rs` displays updated board and game state + +## Architecture Rationale + +### Modularity +Each module has a single, well-defined responsibility following the Single Responsibility Principle. This makes the code easier to understand, test, and maintain. + +### Separation of Concerns +- **Presentation** (main.rs): User interaction +- **Business Logic** (game.rs): Game rules and state +- **Data Structures** (board.rs, types.rs): Core data representations +- **AI Strategy** (ai.rs): Decision-making algorithm + +### Testability +The modular design enables comprehensive unit testing. Each module can be tested independently: +- Game logic tests verify win detection and state transitions +- AI tests verify blocking and winning move selection +- Board tests verify move validation and state management + +### Type Safety +Rust's strong type system ensures correctness: +- Enums prevent invalid player or cell states +- The borrow checker prevents data races +- Pattern matching ensures all cases are handled + +## Usage + +### Building the Project + +```bash +# Clone the repository +git clone +cd topics/tic-tac-toe + +# Build the project +cargo build --release + +# Run the game +cargo run --release +``` + +### Playing the Game + +When you start the game, you'll see a position guide: + +``` + 1 | 2 | 3 + ----------- + 4 | 5 | 6 + ----------- + 7 | 8 | 9 +``` + +Enter numbers 1-9 to place your mark (X) on the board. The AI (O) will respond after each move. + +### Example Game Session + +``` +================================= + Welcome to Tic-Tac-Toe! +================================= + +You are X, AI is O +Enter positions 1-9 as shown: + + 1 | 2 | 3 + ----------- + 4 | 5 | 6 + ----------- + 7 | 8 | 9 + + + | | + ----------- + | | + ----------- + | | + +Your turn (X) +Enter position (1-9): 5 + + | | + ----------- + | X | + ----------- + | | + +AI is thinking... 🤔 +AI played position 1 + + O | | + ----------- + | X | + ----------- + | | + +Your turn (X) +Enter position (1-9): 3 +... +``` + +### Running Tests + +```bash +# Run all tests +cargo test + +# Run tests with output +cargo test -- --nocapture + +# Run specific test +cargo test test_ai_blocks_winning_move +``` + +### Code Quality Checks + +```bash +# Check formatting +cargo fmt --check + +# Format code +cargo fmt + +# Run linter +cargo clippy + +# Build without warnings +cargo build --release +``` + +## Performance Considerations + +### Minimax Optimization +- The game tree is relatively small for Tic-Tac-Toe (maximum 9! = 362,880 possible games) +- Depth-based scoring encourages faster wins, reducing average computation time +- Early terminal state detection prunes unnecessary branches + +### Memory Efficiency +- Board uses a stack-allocated array instead of heap allocation +- Game state cloning for AI simulations is lightweight (72 bytes) +- No dynamic memory allocation during gameplay + +## Future Enhancements + +Potential improvements for future versions: + +1. **Alpha-Beta Pruning**: Further optimize Minimax by skipping branches that cannot affect the final decision +2. **Difficulty Levels**: Add options for easier AI by limiting search depth or introducing randomness +3. **Undo/Redo**: Allow players to rewind moves +4. **Game History**: Save and replay past games +5. **GUI Version**: Create a graphical interface using a framework like `egui` or web-based UI +6. **Network Play**: Enable human vs human over network +7. **Different Board Sizes**: Generalize to NxN boards + +## Conclusion + +This Tic-Tac-Toe implementation demonstrates clean software architecture principles in Rust. The modular design separates concerns effectively, making the codebase maintainable and extensible. The Minimax algorithm ensures optimal AI play, providing a challenging opponent that cannot be beaten. The project showcases Rust's strengths in type safety, performance, and code quality enforcement through its tooling ecosystem. diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index 13cb87f..f19fe6c 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -29,15 +29,15 @@ fn main() { // Check game state match game.state() { GameState::Won(Player::Human) => { - println!("🎉 Congratulations! You won!"); + println!("Congratulations! You won!"); break; } GameState::Won(Player::AI) => { - println!("💻 AI wins! Better luck next time!"); + println!("AI wins! Better luck next time!"); break; } GameState::Draw => { - println!("🤝 It's a draw! Well played!"); + println!("It's a draw! Well played!"); break; } GameState::InProgress => { @@ -52,18 +52,18 @@ fn main() { let position = get_human_move(&game); if !game.make_move(position) { - println!("❌ Invalid move! Try again."); + println!("Invalid move! Try again."); continue; } } else { // AI turn - println!("AI is thinking... 🤔"); + println!("AI is thinking..."); if let Some(position) = ai.find_best_move(&game) { game.make_move(position); println!("AI played position {}", position + 1); } else { - println!("❌ Error: AI couldn't find a move!"); + println!("Error: AI couldn't find a move!"); break; } } @@ -93,11 +93,11 @@ fn get_human_move(game: &Game) -> usize { if game.available_moves().contains(&position) { return position; } else { - println!("❌ That position is already taken! Try another."); + println!("That position is already taken! Try another."); } } _ => { - println!("❌ Invalid input! Please enter a number between 1 and 9."); + println!("Invalid input! Please enter a number between 1 and 9."); } } }