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/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/ai.rs b/topics/tic-tac-toe/src/ai.rs new file mode 100644 index 0000000..48d7a2e --- /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/board.rs b/topics/tic-tac-toe/src/board.rs new file mode 100644 index 0000000..23540b6 --- /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/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 new file mode 100644 index 0000000..f19fe6c --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,113 @@ +mod ai; +mod board; +mod game; +mod types; + +use ai::AI; +use game::{Game, GameState}; +use std::io::{self, Write}; +use types::Player; + +fn main() { + 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"); +} 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(), + } + } +}