diff --git a/topics/tic_tac_toe/.gitignore b/topics/tic_tac_toe/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/topics/tic_tac_toe/Cargo.lock b/topics/tic_tac_toe/Cargo.lock new file mode 100644 index 0000000..9a4a9bd --- /dev/null +++ b/topics/tic_tac_toe/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tic_tac_toe" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/topics/tic_tac_toe/Cargo.toml b/topics/tic_tac_toe/Cargo.toml new file mode 100644 index 0000000..1f066c7 --- /dev/null +++ b/topics/tic_tac_toe/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "tic_tac_toe" +version = "0.1.0" +edition = "2021" +authors = ["evan.paillard@etu.umontpellier.fr"] +description = "A command-line Tic Tac Toe game with an unbeatable AI using the Minimax algorithm" + +[dependencies] +rand = "0.8.5" \ No newline at end of file diff --git a/topics/tic_tac_toe/README_EN.md b/topics/tic_tac_toe/README_EN.md new file mode 100644 index 0000000..07f1c4a --- /dev/null +++ b/topics/tic_tac_toe/README_EN.md @@ -0,0 +1,59 @@ +# Tic Tac Toe with Unbeatable AI + +A command-line Tic Tac Toe game implemented in Rust, featuring an unbeatable AI using the Minimax algorithm. + +## Project Structure + +``` +tic_tac_toe/ +├── src/ +│ ├── board.rs # Game board representation and logic +│ ├── player/ +│ │ ├── mod.rs # Player trait definition +│ │ ├── human.rs # Human player implementation +│ │ └── ai.rs # AI player with Minimax algorithm +│ ├── game.rs # Game state and flow management +│ ├── main.rs # Entry point and user interface +│ └── lib.rs # Library exports +├── tests/ # Unit tests +├── docs/ +│ └── architecture_EN.md # Detailed architecture documentation (EN) +└── README_EN.md +``` + +## How to Run the Project + +1. **Build** + ```bash + cargo build + ``` + +2. **Run** + ```bash + cargo run + ``` + +3. **Test** + ```bash + cargo test + ``` + +## How to Play + +1. Start the game with `cargo run` +2. Enter your name when prompted +3. The game randomly decides who goes first +4. On your turn, enter coordinates as "row column" (e.g., "1 1" for the center) +5. Indices range from 0 to 2, as shown on the board + +## Main Features + +- **Unbeatable AI** using the Minimax algorithm +- Command-line interface with Unicode display +- Modular and object-oriented architecture +- Comprehensive unit tests + +## Documentation + +For detailed documentation of the project architecture, see: +- English version: `docs/architecture_EN.md` diff --git a/topics/tic_tac_toe/docs/architecture_EN.md b/topics/tic_tac_toe/docs/architecture_EN.md new file mode 100644 index 0000000..c2fba8f --- /dev/null +++ b/topics/tic_tac_toe/docs/architecture_EN.md @@ -0,0 +1,480 @@ +# Tic Tac Toe Architecture Documentation + +## Project Definition + +This project is a command-line implementation of the classic Tic Tac Toe game, featuring an unbeatable AI opponent. The AI uses the Minimax algorithm to make optimal decisions, ensuring that it cannot be beaten (it will either win or force a draw). + +The main objectives of this project are: + +1. Create a playable Tic Tac Toe game with a clean command-line interface +2. Implement an unbeatable AI using the Minimax algorithm +3. Demonstrate good software architecture principles in Rust +4. Provide a modular, maintainable, and well-documented codebase + +## Components and Modules + +The project is organized into several modules, each with a specific responsibility: + +### Board Module (`board.rs`) + +Responsible for representing and managing the game board. + +- **Cell Enum**: Represents the state of a cell on the board (Empty, X, O) +- **Board Struct**: Manages a 3x3 grid of cells +- **Key Methods**: + - `new()`: Creates a new empty board + - `get_cell()` / `set_cell()`: Access and modify cells + - `available_moves()`: Lists all valid moves + - `check_winner()`: Determines if a player has won + - `is_full()`: Checks if the board is full (draw) + - `display()`: Renders the board to the console + +### Player Module (`player/`) + +Defines the player interface and implementations. + +- **Player Trait** (`mod.rs`): Interface that all player types must implement + - `get_cell_type()`: Returns the player's cell type (X or O) + - `make_move()`: Determines the next move + - `get_name()`: Returns the player's name + +- **Human Player** (`human.rs`): Implementation for human players + - **HumanPlayer Structure**: Represents a human player + - `cell_type`: Cell type used by the player (X or O) + - `name`: Player's name + - **Key Methods**: + - `new(cell_type, name)`: Creates a new human player instance + - `get_cell_type()`: Trait implementation that returns the player's cell type + - `get_name()`: Trait implementation that returns the player's name + - `make_move(board)`: Trait implementation that: + - Displays the message asking the player to enter a move + - Reads user input from the console + - Validates that the input contains two valid coordinates + - Checks that the coordinates are in the 0-2 range + - Ensures that the selected cell is empty + - Continues to ask until a valid move is obtained + - Returns the coordinates of the validated move + +- **AI Player** (`ai.rs`): Implementation for the AI opponent + - **AIPlayer Structure**: Represents an AI player + - `cell_type`: Cell type used by the AI (X or O) + - `name`: AI player's name + - **Key Methods**: + - `new(cell_type, name)`: Creates a new AI player instance + - `opponent_cell()`: Determines the opponent's cell type (X if AI is O, O if AI is X) + - `minimax(board, depth, is_maximizing)`: Implements the Minimax algorithm that: + - Recursively explores all possible moves + - Evaluates terminal states (win, loss, draw) + - Assigns scores to positions (+10 for win, -10 for loss, 0 for draw) + - Adjusts scores based on depth to favor quick wins + - Alternates between maximization (AI's turn) and minimization (opponent's turn) + - Returns the optimal score for the current position + - `get_cell_type()`: Trait implementation that returns the AI's cell type + - `get_name()`: Trait implementation that returns the AI's name + - `make_move(board)`: Trait implementation that: + - Iterates through all available moves + - Evaluates each move with the Minimax algorithm + - Selects the move with the best score + - Returns the coordinates of the optimal move + +### Game Module (`game.rs`) + +Manages the game state and flow. + +- **GameState Enum**: Represents the current state of the game (InProgress, Win, Draw) +- **Game Struct**: Orchestrates the game flow +- **Key Methods**: + - `new()`: Creates a new game with specified players + - `play_turn()`: Executes a single turn + - `update_state()`: Updates the game state after each move + - `display_result()`: Shows the final result + +### Main Module (`main.rs`) + +Entry point for the application. + +- Handles initial setup +- Creates players (human and AI) +- Manages the game loop +- Handles user interaction + +## Module Interactions + +The Tic Tac Toe game is organized around simple interactions between its different modules. Here's how they communicate with each other: + +### Game Startup + +- The program begins in `main.rs` which creates the players (human and AI) +- A random selection decides who goes first +- The main module then creates the game and starts the main loop + +### Turn Progression + +1. The `Game` module displays the current state of the board using functions from the `Board` module +2. The current player is prompted to make a move: + - The human player enters coordinates via the console + - The AI calculates the best possible move using the Minimax algorithm +3. The `Game` module verifies that the move is valid by consulting the `Board` +4. The board is updated with the new move +5. The `Game` module checks if the game is over: + - Checking for a win (three symbols in a row) + - Checking for a draw (full board) +6. If the game continues, the next player's turn begins + +### Game End + +- Once the game is over, the final result is displayed +- The final board is shown one last time +- A message indicates who won or if there was a draw + +This organization allows for a clear separation of responsibilities: the `Board` manages the state of the board, the `Players` determine the moves, and the `Game` coordinates the overall flow of the game. + +## Why This Modular Architecture? + +I chose to separate the code into distinct modules for several important reasons: + +1. **Logical Code Organization**: + - The separation into modules (`board.rs`, `player/`, `game.rs`) reflects the natural components of a Tic Tac Toe game + - Each file contains a specific aspect of the game, making the code easier to explore and understand + - This structure allows quickly finding where to implement a new feature or fix a bug + +2. **Separation of Concerns**: + - The board (`board.rs`) only handles the game state and rules (win checking, etc.) + - The players (`player/`) focus on game strategy (human via console, AI via Minimax) + - The game (`game.rs`) manages the game flow and turn alternation + - The entry point (`main.rs`) handles initialization and user interface + +3. **Common Interface for Different Player Types**: + - The `Player` trait in `player/mod.rs` defines a contract that all players must follow + - This abstraction allows treating human and AI players uniformly + - It facilitates adding new player types (e.g., a network player or an AI with a different algorithm) + +4. **Decoupling and Flexibility**: + - Modules interact through well-defined interfaces + - The game doesn't need to know the implementation details of the AI + - We can replace the Minimax algorithm with another strategy without modifying the rest of the code + - This independence facilitates future evolution and experimentation + +5. **Component Reusability**: + - The separation into modules allows exporting certain parts as a reusable library + +This code organization makes the project: + +- **Easier to understand**: Each file has a clear role +- **Easier to modify**: We can change one part without touching the rest +- **Easier to test**: We can verify each part separately +- **Easier to evolve**: We can add new features simply + +## Minimax Algorithm + +The Minimax algorithm is implemented in the `ai.rs` module and forms the core of the AI's intelligence. Here's how it works: + +1. The algorithm recursively explores all possible moves until reaching a terminal state (win, loss, or draw) +2. For each terminal state, a score is assigned: + - +10 for an AI win (adjusted by depth to favor quick wins) + - -10 for an AI loss (adjusted by depth to delay losses) + - 0 for a draw +3. These scores are propagated upward in the game tree: + - The maximizing player (AI) chooses the move with the maximum score + - The minimizing player (opponent) chooses the move with the minimum score +4. The move with the best score for the AI is selected + +## Usage Examples + +### Starting the Game + +```bash +cargo run +``` + +### Playing a Move + +When prompted, enter coordinates as row and column, separated by a space: + +``` +Enter your move (row col): 1 1 +``` + +This would place your mark in the center of the board. + +### Game Flow + +1. The game randomly decides who goes first +2. Players take turns making moves +3. The game checks for win/draw conditions after each move +4. When the game ends, the result is displayed + +### Example Game Session + +``` +Welcome to Tic Tac Toe! +You'll be playing against an unbeatable AI using the Minimax algorithm. +Enter your name: Alice +Alice (X) goes first! +Alice's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ │ │ │ + ├───┼───┼───┤ +1 │ │ │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +Enter your move (row col): 1 1 +Alice placed at position (1, 1) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ │ │ │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (0, 0) +Alice's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ O │ │ │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +Enter your move (row col): 2 2 +Alice placed at position (2, 2) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ O │ │ │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ │ │ X │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (0, 2) +Alice's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ O │ │ O │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ │ │ X │ + └───┴───┴───┘ + +Enter your move (row col): 2 0 +Alice placed at position (2, 0) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ O │ │ O │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ X │ │ X │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (0, 1) +AI wins! + 0 1 2 + ┌───┬───┬───┐ +0 │ O │ O │ O │ + ├───┼───┼───┤ +1 │ │ X │ │ + ├───┼───┼───┤ +2 │ X │ │ X │ + └───┴───┴───┘ +``` + +In this example, the AI wins by aligning three Os on the top row (positions 0,0 - 0,1 - 0,2). This demonstrates how the Minimax algorithm allows the AI to play optimally and win when the human player makes suboptimal choices. + +### Draw Example + +Here's an example of a game that ends in a draw when both players play optimally: + +``` +Welcome to Tic Tac Toe! +You'll be playing against an unbeatable AI using the Minimax algorithm. +Enter your name: Bob +Bob (X) goes first! +Bob's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ │ │ │ + ├───┼───┼───┤ +1 │ │ │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +Enter your move (row col): 0 0 +Bob placed at position (0, 0) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ │ │ + ├───┼───┼───┤ +1 │ │ │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (1, 1) +Bob's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ │ │ + ├───┼───┼───┤ +1 │ │ O │ │ + ├───┼───┼───┤ +2 │ │ │ │ + └───┴───┴───┘ + +Enter your move (row col): 2 0 +Bob placed at position (2, 0) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ │ │ + ├───┼───┼───┤ +1 │ │ O │ │ + ├───┼───┼───┤ +2 │ X │ │ │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (0, 2) +Bob's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ │ O │ + ├───┼───┼───┤ +1 │ │ O │ │ + ├───┼───┼───┤ +2 │ X │ │ │ + └───┴───┴───┘ + +Enter your move (row col): 0 1 +Bob placed at position (0, 1) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ X │ O │ + ├───┼───┼───┤ +1 │ │ O │ │ + ├───┼───┼───┤ +2 │ X │ │ │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (2, 2) +Bob's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ X │ O │ + ├───┼───┼───┤ +1 │ │ O │ │ + ├───┼───┼───┤ +2 │ X │ │ O │ + └───┴───┴───┘ + +Enter your move (row col): 1 0 +Bob placed at position (1, 0) +AI's turn (O) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ X │ O │ + ├───┼───┼───┤ +1 │ X │ O │ │ + ├───┼───┼───┤ +2 │ X │ │ O │ + └───┴───┴───┘ + +AI is thinking... +AI placed at position (1, 2) +Bob's turn (X) + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ X │ O │ + ├───┼───┼───┤ +1 │ X │ O │ O │ + ├───┼───┼───┤ +2 │ X │ │ O │ + └───┴───┴───┘ + +Enter your move (row col): 2 1 +Bob placed at position (2, 1) +The game ended in a draw! + 0 1 2 + ┌───┬───┬───┐ +0 │ X │ X │ O │ + ├───┼───┼───┤ +1 │ X │ O │ O │ + ├───┼───┼───┤ +2 │ X │ X │ O │ + └───┴───┴───┘ +``` + +In this example, both players play optimally, resulting in a draw. The human player started with the top-left corner, and the AI responded with the center, following the optimal strategy. This illustrates how the Minimax algorithm ensures that the AI can never lose: it will win if possible, or at least force a draw against a player who plays well. + +## Tests + +I've written tests to verify each part of the game. The tests are organized in the following files: + +### `tests/board_tests.rs` + +This file tests the game board functionality with the following functions: + +- `test_new_board`: Verifies that a new board is empty. +- `test_set_get_cell`: Ensures that cells are correctly updated. +- `test_is_cell_empty`: Confirms that empty cell detection works. +- `test_available_moves`: Verifies that available moves are properly listed. +- `test_check_winner`: Ensures that win conditions are correctly detected. +- `test_is_full`: Tests the detection of a full board (draw). + +### `tests/human_tests.rs` + +This file tests the human player: + +- `test_human_initialization`: Verifies that the human player is correctly initialized. +- `test_human_get_cell_type`: Ensures that the cell type is correctly returned. +- `test_human_get_name`: Verifies that the player's name is correctly returned. +- `test_human_make_move_validation`: Tests the validation of inputs for human player moves. + +### `tests/ai_tests.rs` + +This file tests the artificial intelligence and Minimax algorithm: + +- `test_ai_blocks_win`: Verifies that the AI blocks the opponent when they're about to win. +- `test_ai_takes_win`: Ensures that the AI chooses a winning move when possible. +- `test_ai_optimal_first_move`: Verifies that the AI makes an optimal first move. +- `test_minimax_scores`: Tests the scores assigned by the Minimax algorithm. + +### `tests/game_tests.rs` + +This file tests the overall game flow: + +- `test_game_initialization`: Verifies that the game is correctly initialized. +- `test_switch_player`: Ensures that player alternation works. +- `test_update_state`: Tests the update of the game state after each move. +- `test_game_win_detection`: Verifies that wins are properly detected. +- `test_game_draw_detection`: Ensures that draws are correctly identified. + +To run all the tests: + +```bash +cargo test +``` + +## Conclusion + +This Tic Tac Toe project allowed me to create a simple but complete game with an AI that never loses. I organized the code into modules to make it easy to understand and modify. Using the Minimax algorithm makes the AI very strong, impossible to beat. diff --git a/topics/tic_tac_toe/src/board.rs b/topics/tic_tac_toe/src/board.rs new file mode 100644 index 0000000..1940497 --- /dev/null +++ b/topics/tic_tac_toe/src/board.rs @@ -0,0 +1,161 @@ +/// Énumération représentant l'état d'une cellule sur le plateau de jeu +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cell { + /// Cellule vide + Empty, + /// Cellule occupée par le joueur X + X, + /// Cellule occupée par le joueur O + O, +} + +impl Cell { + /// Fonction qui: Convertit une cellule en caractère pour l'affichage dans la console + /// Returns: Un caractère représentant la cellule (' ' pour Empty, 'X' pour X, 'O' pour O) (char) + pub fn to_char(self) -> char { + + match self { + Cell::Empty => ' ', + Cell::X => 'X', + Cell::O => 'O', + } + } +} + +/// Structure représentant le plateau de jeu du Tic Tac Toe (grille 3x3) +#[derive(Debug, Clone)] +pub struct Board { + /// Grille 3x3 + cells: [[Cell; 3]; 3], +} + +impl Default for Board { + fn default() -> Self { + Self::new() + } +} + +impl Board { + /// Fonction qui: Crée un nouveau plateau de jeu vide + /// Returns: Un nouveau plateau avec toutes les cellules vides (Board) + pub fn new() -> Self { + Self { + cells: [[Cell::Empty; 3]; 3] + } + } + + /// Fonction qui: Récupère l'état d'une cellule à une position donnée + /// Arguments: `row` - L'indice de ligne (0-2), `col` - L'indice de colonne (0-2) + /// Returns: L'état de la cellule à la position spécifiée (cell: Cell) + pub fn get_cell(&self, row: usize, col: usize) -> Cell { + self.cells[row][col] + } + + /// Fonction qui: Définit l'état d'une cellule à une position donnée + /// Arguments: `row` - L'indice de ligne (0-2), `col` - L'indice de colonne (0-2), `cell` - Le nouvel état de la cellule + pub fn set_cell(&mut self, row: usize, col: usize, cell: Cell){ + self.cells[row][col] = cell; + } + + /// Fonction qui: Vérifie si une cellule est vide à une position donnée + /// Arguments: `row` - L'indice de ligne (0-2), `col` - L'indice de colonne (0-2) + /// Returns: `true` si la cellule est vide, `false` sinon + pub fn is_cell_empty(&self, row: usize, col: usize) -> bool { + self.cells[row][col] == Cell::Empty + } + + /// Fonction qui: Récupère la liste des coups disponibles (cellules vides) + /// Returns: Un vecteur de tuples représentant les positions des cellules vides + pub fn available_moves(&self) -> Vec<(usize,usize)> { + let mut moves = Vec::new(); + // Parcourt toutes les cellules du plateau + for row in 0..3{ + for col in 0..3{ + // Ajoute la position à la liste si la cellule est vide + if self.is_cell_empty(row,col){ + moves.push((row,col)); + } + } + } + moves + } + + + /// Fonction qui: Vérifie si le plateau est plein (aucune cellule vide) + /// Returns: `true` si toutes les cellules sont occupées, `false` sinon + pub fn is_full(&self) -> bool { + // Parcourt toutes les cellules du plateau + for row in 0..3{ + for col in 0..3{ + // Si une cellule est vide, le plateau n'est pas plein + if self.is_cell_empty(row,col){ + return false; + } + } + } + + true + } + + /// Fonction qui: Vérifie si un joueur a gagné en alignant trois symboles + /// Arguments: `player` - Le type de cellule du joueur (X ou O) à vérifier + /// Returns: `true` si le joueur a gagné, `false` sinon + pub fn check_winner(&self, player: Cell) -> bool { + + // Vérifie les lignes pour un alignement horizontal + for row in 0..3{ + if self.cells[row][0] == player && self.cells[row][1] == player && self.cells[row][2] == player { + return true; + } + } + + // Vérifie les colonnes pour un alignement vertical + for col in 0..3{ + if self.cells[0][col] == player && self.cells[1][col] == player && self.cells[2][col] == player { + return true; + } + } + + // Vérifie la diagonale principale (haut gauche à bas droite) + if self.cells[0][0] == player && self.cells[1][1] == player && self.cells[2][2] == player{ + return true; + } + + // Vérifie la diagonale secondaire (bas gauche à haut droite) + if self.cells[2][0] == player && self.cells[1][1] == player && self.cells[0][2] == player { + return true; + } + + false + } + + /// Fonction qui: Affiche le plateau de jeu dans la console avec des bordures Unicode + pub fn display(&self) { + println!(" 0 1 2 "); + println!(" ┌───┬───┬───┐"); + for row in 0..3 { + print!("{} │", row); + for col in 0..3 { + let cell = self.get_cell(row, col); + match cell { + Cell::X => print!(" X │"), + Cell::O => print!(" O │"), + Cell::Empty => print!(" │"), + } + } + println!(); + if row < 2 { + println!(" ├───┼───┼───┤"); + } else { + println!(" └───┴───┴───┘"); + } + } + println!(); + } +} + + + + + + diff --git a/topics/tic_tac_toe/src/game.rs b/topics/tic_tac_toe/src/game.rs new file mode 100644 index 0000000..74c8850 --- /dev/null +++ b/topics/tic_tac_toe/src/game.rs @@ -0,0 +1,134 @@ +use crate::board::{Board, Cell}; +use crate::player::Player; + +/// Énumération représentant l'état du jeu +pub enum GameState{ + /// Jeu en cours + InProgress, + /// Victoire d'un joueur, contient le type de cellule du gagnant + Win(Cell), + /// Match nul + Draw, +} + +/// Structure pour la gestion du jeu +pub struct Game{ + /// Plateau de jeu + board: Board, + /// Index du joueur actuel + current_player_index: usize, + /// Liste des joueurs + players: Vec>, + /// État actuel du jeu + state: GameState, +} + +impl Game{ + + /// Fonction qui: Crée une nouvelle partie avec les joueurs spécifiés + /// Arguments: `players` - Vecteur contenant les joueurs (doit contenir exactement 2 joueurs) + /// Returns: Une nouvelle instance de jeu avec un plateau vide (Game) + /// Panics: Si le nombre de joueurs n'est pas égal à 2 + pub fn new(players: Vec>) -> Self { + if players.len() != 2 { + panic!("Game must have exactly 2 players"); + } + + Self { + board: Board::new(), + current_player_index: 0, + players, + state: GameState::InProgress, + } + } + + /// Fonction qui: Retourne une référence au joueur dont c'est le tour + /// Returns: Une référence au joueur actuel (&dyn Player) + pub fn current_player(&self) -> &dyn Player { + self.players[self.current_player_index].as_ref() + } + + /// Fonction qui: Passe au joueur suivant en alternant entre les deux joueurs + pub fn switch_player(&mut self){ + self.current_player_index = (self.current_player_index +1) % self.players.len(); + } + + + /// Fonction qui: Met à jour l'état du jeu (vérifie s'il y a un gagnant ou un match nul) + pub fn update_state(&mut self){ + // Vérifie si un des joueurs a gagné + for player in &self.players{ + // Si un joueur a aligné trois symboles, met à jour l'état du jeu en victoire + if self.board.check_winner(player.get_cell_type()){ + self.state = GameState::Win(player.get_cell_type()); + return; + } + } + + // Si le plateau est plein et qu'aucun joueur n'a gagné, c'est un match nul + if self.board.is_full(){ + self.state = GameState::Draw; + } + } + + /// Fonction qui: Retourne l'état actuel du jeu + /// Returns: Une référence à l'état actuel du jeu (&GameState) + pub fn state(&self) -> &GameState{ + &self.state + } + + /// Fonction qui: Retourne une référence mutable au plateau de jeu (pour les tests) + /// Returns: Une référence mutable au plateau de jeu (&mut Board) + #[allow(dead_code)] + pub fn get_board_mut(&mut self) -> &mut Board { + &mut self.board + } + + /// Fonction qui: Exécute un tour de jeu complet (affichage, coup, mise à jour) + pub fn play_turn(&mut self) { + let player_name = self.current_player().get_name().to_string(); + let cell_type = self.current_player().get_cell_type(); + let cell_char = cell_type.to_char(); + + println!("{}'s turn ({})", player_name, cell_char); + + self.board.display(); + + let (row, col) = self.current_player().make_move(&self.board); + + self.board.set_cell(row, col, cell_type); + + println!("{} placed at position ({}, {})", player_name, row, col); + + self.update_state(); + // Passe au joueur suivant seulement si le jeu est toujours en cours + if matches!(self.state, GameState::InProgress) { + self.switch_player(); + } + } + + /// Fonction qui: Affiche le résultat final de la partie (victoire ou match nul) + pub fn display_result(&self) { + self.board.display(); + + // Affiche un message différent selon l'état final du jeu + match &self.state { + // En cas de victoire, trouve le joueur gagnant et affiche son nom + GameState::Win(cell) => { + let winner = self.players.iter() + .find(|p| p.get_cell_type() == *cell) + .unwrap(); + println!("{} wins!", winner.get_name()); + } + // En cas de match nul, affiche un message approprié + GameState::Draw => { + println!("The game ended in a draw."); + } + // Si le jeu est encore en cours (cas rare ici), affiche un message d'information + GameState::InProgress => { + println!("The game is still in progress."); + } + } + } + +} \ No newline at end of file diff --git a/topics/tic_tac_toe/src/lib.rs b/topics/tic_tac_toe/src/lib.rs new file mode 100644 index 0000000..8bbad1b --- /dev/null +++ b/topics/tic_tac_toe/src/lib.rs @@ -0,0 +1,18 @@ +/// Module pour la gestion du plateau de jeu +pub mod board; +/// Module pour la gestion des joueurs (humain et IA) +pub mod player; +/// Module pour la gestion du déroulement du jeu +pub mod game; + +// Réexportation des types principaux pour faciliter leur utilisation +/// Type de cellule (Empty, X, O) +pub use board::Cell; +/// Interface commune à tous les types de joueurs +pub use player::Player; +/// Joueur humain qui interagit via la console +pub use player::human::HumanPlayer; +/// Joueur IA utilisant l'algorithme Minimax +pub use player::ai::AIPlayer; +/// Structure de jeu et énumération des états possibles +pub use game::{Game, GameState}; diff --git a/topics/tic_tac_toe/src/main.rs b/topics/tic_tac_toe/src/main.rs new file mode 100644 index 0000000..adbc2cb --- /dev/null +++ b/topics/tic_tac_toe/src/main.rs @@ -0,0 +1,50 @@ +/// Module pour la gestion du plateau de jeu +mod board; +/// Module pour la gestion des joueurs (humain et IA) +mod player; +/// Module pour la gestion du déroulement du jeu +mod game; + +use std::io::{self, Write}; +use rand::Rng; +use board::Cell; +use player::human::HumanPlayer; +use player::ai::AIPlayer; +use game::{Game, GameState}; + +/// Point d'entrée du programme +fn main() { + println!("Welcome to Tic Tac Toe"); + println!("You'll be playing against an unbeatable AI using the Minimax algorithm."); + + print!("Enter your name: "); + io::stdout().flush().unwrap(); + let mut name = String::new(); + io::stdin().read_line(&mut name).unwrap(); + let name = name.trim().to_string(); + + let mut rng = rand::thread_rng(); + let human_goes_first = rng.gen_bool(0.5); + + let players: Vec> = if human_goes_first { + println!("{} (X) goes first", name); + vec![ + Box::new(HumanPlayer::new(Cell::X, name)), + Box::new(AIPlayer::new(Cell::O, "AI".to_string())), + ] + } else { + println!("AI (X) goes first"); + vec![ + Box::new(AIPlayer::new(Cell::X, "AI".to_string())), + Box::new(HumanPlayer::new(Cell::O, name)), + ] + }; + + let mut game = Game::new(players); + + while matches!(game.state(), GameState::InProgress) { + game.play_turn(); + } + + game.display_result(); +} diff --git a/topics/tic_tac_toe/src/player/ai.rs b/topics/tic_tac_toe/src/player/ai.rs new file mode 100644 index 0000000..ab33b88 --- /dev/null +++ b/topics/tic_tac_toe/src/player/ai.rs @@ -0,0 +1,123 @@ +use crate::board::{Board, Cell}; +use super::Player; + +/// Joueur IA utilisant l'algorithme Minimax +pub struct AIPlayer { + /// Type de cellule du joueur + cell_type: Cell, + /// Nom du joueur + name: String, +} + +impl AIPlayer { + /// Fonction qui: Crée un nouveau joueur IA avec le type de cellule et le nom spécifiés + /// Arguments: `cell_type` - Le type de cellule du joueur, `name` - Le nom du joueur + /// Returns: Une nouvelle instance de joueur IA (AIPlayer) + pub fn new(cell_type: Cell, name: String) -> Self { + Self { cell_type, name } + } + + /// Fonction qui: Retourne le type de cellule de l'adversaire + /// Returns: Le type de cellule de l'adversaire (X si l'IA est O, O si l'IA est X) (Cell) + pub fn opponent_cell(&self) -> Cell { + // Détermine le type de cellule de l'adversaire en fonction du type de cellule de l'IA + match self.cell_type { + Cell::X => Cell::O, + Cell::O => Cell::X, + Cell::Empty => panic!("AI player cannot have Empty cell type"), + } + } + + /// Fonction qui: Implémente l'algorithme Minimax pour déterminer le meilleur coup + /// Arguments: `board` - État du plateau, `depth` - Profondeur actuelle, `is_maximizing` - Tour du joueur maximisant + /// Returns: Score de la position (-10 à +10) (score: i32) + pub fn minimax(&self, board: &Board, depth: i32, is_maximizing: bool) -> i32 { + let ai_cell = self.cell_type; + let human_cell = self.opponent_cell(); + + // Vérifie les états terminaux du jeu et retourne le score approprié + if board.check_winner(ai_cell) { + // L'IA a gagné, retourne un score positif (ajusté par la profondeur) + return 10 - depth; + } + if board.check_winner(human_cell) { + // L'humain a gagné, retourne un score négatif (ajusté par la profondeur) + return depth - 10; + } + if board.is_full() { + // Match nul, retourne un score neutre + return 0; + } + + // Calcule récursivement le meilleur score pour chaque coup possible + if is_maximizing { + // Tour du joueur maximisant (IA) - cherche le score maximum + let mut best_score = i32::MIN; + for (row, col) in board.available_moves() { + // Simule le coup de l'IA + let mut new_board = board.clone(); + new_board.set_cell(row, col, ai_cell); + // Calcule récursivement le score pour ce coup + let score = self.minimax(&new_board, depth + 1, false); + // Met à jour le meilleur score + best_score = best_score.max(score); + } + best_score + } else { + // Tour du joueur minimisant (humain) - cherche le score minimum + let mut best_score = i32::MAX; + for (row, col) in board.available_moves() { + // Simule le coup de l'humain + let mut new_board = board.clone(); + new_board.set_cell(row, col, human_cell); + // Calcule récursivement le score pour ce coup + let score = self.minimax(&new_board, depth + 1, true); + // Met à jour le meilleur score + best_score = best_score.min(score); + } + best_score + } + } +} + +impl Player for AIPlayer { + /// Fonction qui: Retourne le type de cellule du joueur IA + /// Returns: Le type de cellule du joueur (X ou O) (cell_type: Cell) + fn get_cell_type(&self) -> Cell { + self.cell_type + } + + /// Fonction qui: Retourne le nom du joueur IA + /// Returns: Le nom du joueur (name: &str) + fn get_name(&self) -> &str { + &self.name + } + + /// Fonction qui: Détermine le meilleur coup à jouer en utilisant l'algorithme Minimax + /// Arguments: `board` - État actuel du plateau + /// Returns: Un tuple représentant la position du meilleur coup (row: usize, col: usize) + fn make_move(&self, board: &Board) -> (usize, usize) { + println!("{} is thinking...", self.name); + + let mut best_score = i32::MIN; + let mut best_move = (0, 0); + + // Parcourt tous les coups disponibles pour trouver celui avec le meilleur score + for (row, col) in board.available_moves() { + // Simule le coup de l'IA sur une copie du plateau + let mut new_board = board.clone(); + new_board.set_cell(row, col, self.cell_type); + // Calcule le score pour ce coup en utilisant l'algorithme Minimax + let score = self.minimax(&new_board, 0, false); + + // Met à jour le meilleur coup si le score est supérieur + if score > best_score { + best_score = score; + best_move = (row, col); + } + } + + best_move + } +} + diff --git a/topics/tic_tac_toe/src/player/human.rs b/topics/tic_tac_toe/src/player/human.rs new file mode 100644 index 0000000..d1bc857 --- /dev/null +++ b/topics/tic_tac_toe/src/player/human.rs @@ -0,0 +1,86 @@ +use std::io::{self, Write}; +use crate::board::{Board, Cell}; +use super::Player; + +/// Structure représentant un joueur humain +pub struct HumanPlayer { + /// Type de cellule du joueur + cell_type: Cell, + /// Nom du joueur + name: String, +} + + +impl HumanPlayer { + /// Fonction qui: Crée un nouveau joueur humain avec le type de cellule et le nom spécifiés + /// Arguments: `cell_type` - Le type de cellule du joueur, `name` - Le nom du joueur + /// Returns: Une nouvelle instance de joueur humain (HumanPlayer) + pub fn new(cell_type: Cell, name: String) -> Self { + Self { cell_type, name } + } +} + + +impl Player for HumanPlayer { + /// Fonction qui: Retourne le type de cellule du joueur humain + /// Returns: Le type de cellule du joueur (X ou O) (cell_type: Cell) + fn get_cell_type(&self) -> Cell { + self.cell_type + } + + /// Fonction qui: Retourne le nom du joueur humain + /// Returns: Le nom du joueur (name: &str) + fn get_name(&self) -> &str { + &self.name + } + + /// Fonction qui: Demande au joueur humain d'entrer un coup valide via la console + /// Arguments: `board` - Référence au plateau de jeu actuel + /// Returns: Un tuple représentant la position du coup (row: usize, col: usize) + fn make_move(&self, board: &Board) -> (usize, usize) { + + + loop{ + print!("Enter your move (row col): "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + + let coords: Vec<&str> = input.split_whitespace().collect(); + + // Vérifie si l'utilisateur a bien entré exactement deux coordonnées + if coords.len() != 2 { + println!("Please enter two numbers separated by a space."); + continue; + } + + // Convertit la première coordonnée en indice de ligne et vérifie qu'elle est valide (entre 0 et 2) + let row = match coords[0].parse::() { + Ok(num) if num < 3 => num, + _ => { + println!("Row must be a number between 0 and 2."); + continue; + } + }; + + // Convertit la deuxième coordonnée en indice de colonne et vérifie qu'elle est valide (entre 0 et 2) + let col = match coords[1].parse::() { + Ok(num) if num < 3 => num, + _ => { + println!("Column must be a number between 0 and 2."); + continue; + } + }; + + // Vérifie si la cellule sélectionnée est vide et disponible + if !board.is_cell_empty(row, col) { + println!("That cell is already taken. Try again."); + continue; + } + + return (row, col); + + } + } +} diff --git a/topics/tic_tac_toe/src/player/mod.rs b/topics/tic_tac_toe/src/player/mod.rs new file mode 100644 index 0000000..7949cd6 --- /dev/null +++ b/topics/tic_tac_toe/src/player/mod.rs @@ -0,0 +1,23 @@ +use crate::board::{Board, Cell}; + +/// Module pour l'implémentation du joueur humain +pub mod human; +/// Module pour l'implémentation du joueur IA +pub mod ai; + +/// Trait définissant l'interface commune à tous les types de joueurs (IA et Humain) +pub trait Player { + + /// Fonction qui: Retourne le type de cellule du joueur + /// Returns: Le type de cellule utilisé par le joueur (X ou O) (cell_type: Cell) + fn get_cell_type(&self) -> Cell; + + /// Fonction qui: Détermine le prochain coup à jouer sur le plateau + /// Arguments: `board` - Référence au plateau de jeu actuel + /// Returns: Un tuple représentant la position du coup à jouer (row: usize, col: usize) + fn make_move(&self, board: &Board) -> (usize, usize); + + /// Fonction qui: Retourne le nom du joueur + /// Returns: Le nom du joueur sous forme de référence à une chaîne de caractères (name: &str) + fn get_name(&self) -> &str; +} \ No newline at end of file diff --git a/topics/tic_tac_toe/tests/ai_test.rs b/topics/tic_tac_toe/tests/ai_test.rs new file mode 100644 index 0000000..6755215 --- /dev/null +++ b/topics/tic_tac_toe/tests/ai_test.rs @@ -0,0 +1,105 @@ +#[cfg(test)] +mod ai_player_tests { + + use tic_tac_toe::board::{Board, Cell}; + use tic_tac_toe::player::Player; + use tic_tac_toe::player::ai::AIPlayer; + + /// Test: création d'un joueur IA et vérification de ses propriétés + #[test] + fn test_ai_player_creation(){ + let player = AIPlayer::new(Cell::X, "AI".to_string()); + assert_eq!(player.get_cell_type(),Cell::X); + assert_eq!(player.get_name(),"AI"); + } + + /// Test: l'IA bloque un coup gagnant de l'adversaire + #[test] + fn test_ai_block_winning_move(){ + let mut board = Board::new(); + let player = AIPlayer::new(Cell::X, "AI".to_string()); + + board.set_cell(0,0,Cell::O); + board.set_cell(0,1,Cell::O); + + let (row,col) = player.make_move(&board); + assert_eq!((row,col),(0,2)); + } + + /// Test: l'IA choisit un coup gagnant quand c'est possible + #[test] + fn test_ai_makes_winning_move() { + let mut board = Board::new(); + let ai = AIPlayer::new(Cell::X, "AI".to_string()); + + board.set_cell(0, 0, Cell::X); + board.set_cell(0, 1, Cell::X); + board.set_cell(1, 0, Cell::O); + board.set_cell(1, 1, Cell::O); + + let (row, col) = ai.make_move(&board); + assert_eq!((row, col), (0, 2)); + } + + /// Test: l'algorithme Minimax évalue correctement les positions gagnantes et perdantes + #[test] + fn test_minimax_algorithm() { + let ai = AIPlayer::new(Cell::X, "AI".to_string()); + + let mut board = Board::new(); + board.set_cell(0, 0, Cell::X); + board.set_cell(0, 1, Cell::X); + + let score = ai.minimax(&board, 0, true); + assert!(score > 0, "Minimax should return a positive score for a winning position"); + + let mut board = Board::new(); + board.set_cell(0, 0, Cell::O); + board.set_cell(0, 1, Cell::O); + + let score = ai.minimax(&board, 0, true); + assert!(score < 0, "Minimax should return a negative score for a losing position"); + + let board = Board::new(); + let score = ai.minimax(&board, 0, true); + assert!(score > -5 && score < 5, "Minimax should return a moderate score for an empty board"); + } + + /// Test: l'algorithme Minimax retourne un score approprié pour un plateau vide + #[test] + fn test_minimax_empty_board() { + let board = Board::new(); + let ai = AIPlayer::new(Cell::X, "AI".to_string()); + + let score = ai.minimax(&board, 0, true); + assert!(score > -5 && score < 5, "Minimax should return a moderate score for an empty board"); + } + + /// Test: la fonction opponent_cell retourne le type de cellule opposé + #[test] + fn test_opponent_cell() { + let ai_x = AIPlayer::new(Cell::X, "AI_X".to_string()); + let ai_o = AIPlayer::new(Cell::O, "AI_O".to_string()); + + assert_eq!(ai_x.opponent_cell(), Cell::O); + assert_eq!(ai_o.opponent_cell(), Cell::X); + } + + /// Test: la fonction get_cell_type retourne le bon type de cellule + #[test] + fn test_get_cell_type() { + let ai_x = AIPlayer::new(Cell::X, "AI_X".to_string()); + let ai_o = AIPlayer::new(Cell::O, "AI_O".to_string()); + + assert_eq!(ai_x.get_cell_type(), Cell::X); + assert_eq!(ai_o.get_cell_type(), Cell::O); + } + + /// Test: la fonction get_name retourne le bon nom du joueur + #[test] + fn test_get_name() { + let ai = AIPlayer::new(Cell::X, "AI_Player".to_string()); + + assert_eq!(ai.get_name(), "AI_Player"); + } +} diff --git a/topics/tic_tac_toe/tests/board_tests.rs b/topics/tic_tac_toe/tests/board_tests.rs new file mode 100644 index 0000000..dbd05f8 --- /dev/null +++ b/topics/tic_tac_toe/tests/board_tests.rs @@ -0,0 +1,121 @@ +#[cfg(test)] +mod board_tests { + + use tic_tac_toe::board::{Board, Cell}; + + /// Test: un nouveau plateau est entièrement vide + #[test] + fn test_new_board_is_empty(){ + let board = Board::new(); + for row in 0..3{ + for col in 0..3{ + assert_eq!(board.get_cell(row,col), Cell::Empty); + } + } + } + + /// Test: les fonctions set_cell et get_cell modifient et récupèrent correctement les cellules + #[test] + fn test_set_and_get_cell(){ + let mut board = Board::new(); + board.set_cell(1,1, Cell::X); + assert_eq!(board.get_cell(1,1), Cell::X); + assert_eq!(board.get_cell(0,0), Cell::Empty); + } + + /// Test: la fonction is_cell_empty détecte correctement si une cellule est vide + #[test] + fn test_is_cell_empty(){ + let mut board = Board::new(); + assert!(board.is_cell_empty(0,0)); + board.set_cell(0,0, Cell::X); + assert!(!board.is_cell_empty(0,0)); + } + + /// Test: la fonction is_full détecte correctement si le plateau est plein + #[test] + fn test_is_full(){ + let mut board = Board::new(); + assert!(!board.is_full()); + + for row in 0..3{ + for col in 0..3{ + board.set_cell(row,col,Cell::X); + } + } + assert!(board.is_full()); + } + + /// Test: la fonction check_winner détecte correctement une victoire par ligne + #[test] + fn test_check_winner_row(){ + let mut board = Board::new(); + assert!(!board.check_winner(Cell::X)); + + board.set_cell(0,0, Cell::X); + board.set_cell(0,1, Cell::X); + board.set_cell(0,2, Cell::X); + + assert!(board.check_winner(Cell::X)); + assert!(!board.check_winner(Cell::O)); + } + + /// Test: la fonction check_winner détecte correctement une victoire par colonne + #[test] + fn test_check_winner_col(){ + + let mut board = Board::new(); + assert!(!board.check_winner(Cell::X)); + + board.set_cell(0,0, Cell::X); + board.set_cell(1,0, Cell::X); + board.set_cell(2,0, Cell::X); + + assert!(board.check_winner(Cell::X)); + assert!(!board.check_winner(Cell::O)); + + } + + /// Test: la fonction check_winner détecte correctement une victoire par diagonale + #[test] + fn test_check_winner_diag(){ + + let mut board = Board::new(); + assert!(!board.check_winner(Cell::X)); + + board.set_cell(0,0, Cell::X); + board.set_cell(1,1, Cell::X); + board.set_cell(2,2, Cell::X); + + assert!(board.check_winner(Cell::X)); + + let mut board = Board::new(); + + board.set_cell(0,2, Cell::X); + board.set_cell(1,1, Cell::X); + board.set_cell(2,0, Cell::X); + + assert!(board.check_winner(Cell::X)); + } + + /// Test: la fonction available_moves retourne correctement les coups disponibles + #[test] + fn test_available_moove(){ + let mut board = Board::new(); + assert_eq!(board.available_moves().len(),9); + + board.set_cell(0,0, Cell::X); + board.set_cell(1,1, Cell::X); + + let moves = board.available_moves(); + assert_eq!(moves.len(),7); + } + + /// Test: la fonction to_char convertit correctement les cellules en caractères + #[test] + fn test_cell_to_char() { + assert_eq!(Cell::Empty.to_char(), ' '); + assert_eq!(Cell::X.to_char(), 'X'); + assert_eq!(Cell::O.to_char(), 'O'); + } +} \ No newline at end of file diff --git a/topics/tic_tac_toe/tests/game_test.rs b/topics/tic_tac_toe/tests/game_test.rs new file mode 100644 index 0000000..c471eb7 --- /dev/null +++ b/topics/tic_tac_toe/tests/game_test.rs @@ -0,0 +1,205 @@ +#[cfg(test)] +mod game_tests { + use tic_tac_toe::board::{Board, Cell}; + use tic_tac_toe::game::{Game, GameState}; + use tic_tac_toe::player::Player; + + /// Joueur factice pour les tests + struct MockPlayer { + cell_type: Cell, + name: String, + next_move: (usize, usize), + } + + impl Player for MockPlayer { + fn get_cell_type(&self) -> Cell { + self.cell_type + } + + fn get_name(&self) -> &str { + &self.name + } + + fn make_move(&self, _board: &Board) -> (usize, usize) { + self.next_move + } + } + + /// Test: la création d'un jeu initialise l'état + #[test] + fn test_game_creation() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 1), + }); + + let players: Vec> = vec![player1, player2]; + let game = Game::new(players); + + assert!(matches!(game.state(), GameState::InProgress)); + } + + /// Test: la fonction switch_player alterne entre les joueurs + #[test] + fn test_game_switch_player() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 1), + }); + + let players: Vec> = vec![player1, player2]; + let mut game = Game::new(players); + + let first_player_cell = game.current_player().get_cell_type(); + game.switch_player(); + let second_player_cell = game.current_player().get_cell_type(); + + assert_ne!(first_player_cell, second_player_cell); + } + + /// Test: la fonction update_state détecte une victoire + #[test] + fn test_game_win_detection() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 0), + }); + + let players: Vec> = vec![player1, player2]; + let mut game = Game::new(players); + + game.get_board_mut().set_cell(0, 0, Cell::X); + game.get_board_mut().set_cell(0, 1, Cell::X); + game.get_board_mut().set_cell(0, 2, Cell::X); + + game.update_state(); + + assert!(matches!(game.state(), GameState::Win(Cell::X))); + } + + /// Test: la fonction update_state détecte un match nul + #[test] + fn test_game_draw_detection() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 0), + }); + + let players: Vec> = vec![player1, player2]; + let mut game = Game::new(players); + + game.get_board_mut().set_cell(0, 0, Cell::X); + game.get_board_mut().set_cell(0, 1, Cell::O); + game.get_board_mut().set_cell(0, 2, Cell::X); + game.get_board_mut().set_cell(1, 0, Cell::X); + game.get_board_mut().set_cell(1, 1, Cell::O); + game.get_board_mut().set_cell(1, 2, Cell::X); + game.get_board_mut().set_cell(2, 0, Cell::O); + game.get_board_mut().set_cell(2, 1, Cell::X); + game.get_board_mut().set_cell(2, 2, Cell::O); + + game.update_state(); + + assert!(matches!(game.state(), GameState::Draw)); + } + + /// Test: la fonction play_turn exécute un tour de jeu + #[test] + fn test_game_play_turn() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 1), + }); + + let players: Vec> = vec![player1, player2]; + let mut game = Game::new(players); + + assert!(matches!(game.state(), GameState::InProgress)); + + game.play_turn(); + + assert_eq!(game.get_board_mut().get_cell(0, 0), Cell::X); + + assert_eq!(game.current_player().get_cell_type(), Cell::O); + } + + /// Test: scénario complet d'une partie jusqu'à la victoire d'un joueur + #[test] + fn test_full_game_scenario() { + let player1 = Box::new(MockPlayer { + cell_type: Cell::X, + name: "Player1".to_string(), + next_move: (0, 0), + }); + + let player2 = Box::new(MockPlayer { + cell_type: Cell::O, + name: "Player2".to_string(), + next_move: (1, 0), + }); + + let players: Vec> = vec![player1, player2]; + let mut game = Game::new(players); + + fn play_turn_with_move(game: &mut Game, row: usize, col: usize) { + let cell_type = game.current_player().get_cell_type(); + game.get_board_mut().set_cell(row, col, cell_type); + + game.update_state(); + if matches!(game.state(), GameState::InProgress) { + game.switch_player(); + } + } + + play_turn_with_move(&mut game, 0, 0); + assert!(matches!(game.state(), GameState::InProgress)); + + play_turn_with_move(&mut game, 1, 0); + assert!(matches!(game.state(), GameState::InProgress)); + + play_turn_with_move(&mut game, 0, 1); + assert!(matches!(game.state(), GameState::InProgress)); + + play_turn_with_move(&mut game, 1, 1); + assert!(matches!(game.state(), GameState::InProgress)); + + play_turn_with_move(&mut game, 0, 2); + + assert!(matches!(game.state(), GameState::Win(Cell::X))); + } +} \ No newline at end of file diff --git a/topics/tic_tac_toe/tests/human_player_test.rs b/topics/tic_tac_toe/tests/human_player_test.rs new file mode 100644 index 0000000..c6d255b --- /dev/null +++ b/topics/tic_tac_toe/tests/human_player_test.rs @@ -0,0 +1,67 @@ +#[cfg(test)] +mod human_player_tests { + use tic_tac_toe::board::{Board, Cell}; + use tic_tac_toe::player::Player; + use tic_tac_toe::player::human::HumanPlayer; + + /// Joueur factice pour les tests + struct MockHumanPlayer { + cell_type: Cell, + name: String, + next_move: (usize, usize), + } + + impl Player for MockHumanPlayer { + fn get_cell_type(&self) -> Cell { + self.cell_type + } + + fn get_name(&self) -> &str { + &self.name + } + + fn make_move(&self, _board: &Board) -> (usize, usize) { + self.next_move + } + } + + /// Test: la création d'un joueur humain initialise ses propriétés + #[test] + fn test_human_player_creation() { + let human = HumanPlayer::new(Cell::X, "Alice".to_string()); + assert_eq!(human.get_cell_type(), Cell::X); + assert_eq!(human.get_name(), "Alice"); + } + + /// Test: le joueur factice retourne le coup prédéfini + #[test] + fn test_mock_human_player_move() { + let board = Board::new(); + let mock_human = MockHumanPlayer { + cell_type: Cell::O, + name: "MockHuman".to_string(), + next_move: (1, 2), + }; + + let (row, col) = mock_human.make_move(&board); + assert_eq!((row, col), (1, 2)); + } + + /// Test: la fonction get_cell_type retourne le bon type de cellule + #[test] + fn test_get_cell_type() { + let human_x = HumanPlayer::new(Cell::X, "Player_X".to_string()); + let human_o = HumanPlayer::new(Cell::O, "Player_O".to_string()); + + assert_eq!(human_x.get_cell_type(), Cell::X); + assert_eq!(human_o.get_cell_type(), Cell::O); + } + + /// Test: la fonction get_name retourne le bon nom du joueur + #[test] + fn test_get_name() { + let human = HumanPlayer::new(Cell::X, "Human_Player".to_string()); + + assert_eq!(human.get_name(), "Human_Player"); + } +}