Skip to content

Commit 7031bb8

Browse files
feat: add documentation
Signed-off-by: auriane.pusel <auriane.pusel@etu.umontpellier.fr>
1 parent 1c1bc2b commit 7031bb8

File tree

2 files changed

+311
-8
lines changed

2 files changed

+311
-8
lines changed
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# Tic-Tac-Toe AI Agent - Architecture Documentation
2+
3+
## Project Definition
4+
5+
### What is it?
6+
7+
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.
8+
9+
### Goals
10+
11+
The primary goals of this project are:
12+
13+
1. **Unbeatable AI**: Implement an AI that plays optimally using the Minimax algorithm with depth-first search
14+
2. **Interactive Gameplay**: Provide a user-friendly command-line interface for humans to play against the AI
15+
3. **Clean Architecture**: Organize the code into well-defined, modular components
16+
4. **Code Quality**: Follow Rust best practices with proper error handling, formatting, and comprehensive testing
17+
18+
## Components and Modules
19+
20+
The project is structured into five main modules, each with a specific responsibility:
21+
22+
### 1. `types.rs` - Core Type Definitions
23+
24+
**Purpose**: Defines the fundamental types used throughout the application.
25+
26+
**Key Components**:
27+
- `Player` enum: Represents either Human (X) or AI (O) player
28+
- `opponent()`: Returns the opposing player
29+
- `symbol()`: Returns the character representation ('X' or 'O')
30+
- `Cell` enum: Represents a board cell state (Empty or Occupied by a player)
31+
- `is_empty()`: Checks if the cell is available
32+
- `symbol()`: Returns the display character
33+
34+
**Rationale**: Separating type definitions provides a single source of truth for core concepts and enables type safety throughout the codebase.
35+
36+
### 2. `board.rs` - Game Board Representation
37+
38+
**Purpose**: Manages the 3x3 game board state and provides board manipulation operations.
39+
40+
**Key Components**:
41+
- `Board` struct: Internally uses a 1D array of 9 cells for efficient storage
42+
- Key methods:
43+
- `new()`: Creates an empty board
44+
- `make_move(position, player)`: Places a player's mark at a position
45+
- `available_moves()`: Returns all empty positions
46+
- `is_full()`: Checks if the board is completely filled
47+
- `display()`: Renders the board to the console
48+
- `get(position)`: Retrieves the cell state at a position
49+
- `cells()`: Provides access to the internal cell array
50+
51+
**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.
52+
53+
### 3. `game.rs` - Game Logic and State Management
54+
55+
**Purpose**: Implements game rules, win detection, and state transitions.
56+
57+
**Key Components**:
58+
- `GameState` enum: Tracks the current game status
59+
- `InProgress`: Game is ongoing
60+
- `Won(Player)`: A player has won
61+
- `Draw`: Game ended in a draw
62+
- `Game` struct: Orchestrates the overall game flow
63+
- Key methods:
64+
- `new()`: Initializes a new game with Human starting
65+
- `from_board(board, player)`: Creates a game from an existing board state (used by AI simulations)
66+
- `make_move(position)`: Executes a move and updates game state
67+
- `check_winner(player)`: Checks all win conditions (rows, columns, diagonals)
68+
- `evaluate()`: Returns a score for the current board state (+10 for AI win, -10 for Human win, 0 otherwise)
69+
- `update_state()`: Updates the game state after each move
70+
71+
**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.
72+
73+
### 4. `ai.rs` - Minimax AI Implementation
74+
75+
**Purpose**: Implements the AI player using the Minimax algorithm.
76+
77+
**Key Components**:
78+
- `AI` struct: Represents the AI player
79+
- Key methods:
80+
- `find_best_move(game)`: Finds the optimal move for the current game state
81+
- `minimax(game, depth, is_maximizing)`: Recursive Minimax algorithm implementation
82+
- `simulate_move(game, position, player)`: Creates a hypothetical future game state
83+
- `create_game_from_board(board, player)`: Helper for game state creation
84+
85+
**Algorithm Details**:
86+
- **Minimax with Depth Optimization**: The algorithm explores all possible future game states recursively
87+
- Maximizing player (AI): Chooses moves that maximize the score
88+
- Minimizing player (Human): Assumes the opponent plays optimally to minimize AI's score
89+
- Depth consideration: Prefers faster wins (score - depth) and slower losses (score + depth)
90+
- **Terminal States**:
91+
- AI wins: +10
92+
- Human wins: -10
93+
- Draw: 0
94+
95+
**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.
96+
97+
### 5. `main.rs` - User Interface and Game Loop
98+
99+
**Purpose**: Provides the command-line interface and coordinates the game flow.
100+
101+
**Key Components**:
102+
- `main()`: Main game loop that alternates between human and AI turns
103+
- `get_human_move(game)`: Handles user input with validation
104+
- `display_position_guide()`: Shows position numbering (1-9)
105+
106+
**User Experience Features**:
107+
- Clear visual position guide
108+
- Input validation (1-9 range, position availability)
109+
- Informative error messages
110+
- Game result announcements with emojis
111+
- AI thinking indicator
112+
113+
**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.
114+
115+
## Module Interaction Flow
116+
117+
```
118+
┌─────────────────────────────────────────────────────────┐
119+
│ main.rs │
120+
│ (User Interface) │
121+
└────────────┬────────────────────────────┬───────────────┘
122+
│ │
123+
▼ ▼
124+
┌─────────────┐ ┌─────────────┐
125+
│ game.rs │ │ ai.rs │
126+
│ (Game Logic)│◄────────────┤ (AI Player) │
127+
└──────┬──────┘ └─────────────┘
128+
129+
130+
┌─────────────┐
131+
│ board.rs │
132+
│ (Board) │
133+
└──────┬──────┘
134+
135+
136+
┌─────────────┐
137+
│ types.rs │
138+
│ (Types) │
139+
└─────────────┘
140+
```
141+
142+
**Data Flow**:
143+
1. `main.rs` creates a `Game` instance and an `AI` instance
144+
2. For human turns: `main.rs` gets input → validates → calls `game.make_move()`
145+
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()`
146+
4. `Game` updates the `Board` and checks win conditions
147+
5. `main.rs` displays updated board and game state
148+
149+
## Architecture Rationale
150+
151+
### Modularity
152+
Each module has a single, well-defined responsibility following the Single Responsibility Principle. This makes the code easier to understand, test, and maintain.
153+
154+
### Separation of Concerns
155+
- **Presentation** (main.rs): User interaction
156+
- **Business Logic** (game.rs): Game rules and state
157+
- **Data Structures** (board.rs, types.rs): Core data representations
158+
- **AI Strategy** (ai.rs): Decision-making algorithm
159+
160+
### Testability
161+
The modular design enables comprehensive unit testing. Each module can be tested independently:
162+
- Game logic tests verify win detection and state transitions
163+
- AI tests verify blocking and winning move selection
164+
- Board tests verify move validation and state management
165+
166+
### Type Safety
167+
Rust's strong type system ensures correctness:
168+
- Enums prevent invalid player or cell states
169+
- The borrow checker prevents data races
170+
- Pattern matching ensures all cases are handled
171+
172+
## Usage
173+
174+
### Building the Project
175+
176+
```bash
177+
# Clone the repository
178+
git clone <repository-url>
179+
cd topics/tic-tac-toe
180+
181+
# Build the project
182+
cargo build --release
183+
184+
# Run the game
185+
cargo run --release
186+
```
187+
188+
### Playing the Game
189+
190+
When you start the game, you'll see a position guide:
191+
192+
```
193+
1 | 2 | 3
194+
-----------
195+
4 | 5 | 6
196+
-----------
197+
7 | 8 | 9
198+
```
199+
200+
Enter numbers 1-9 to place your mark (X) on the board. The AI (O) will respond after each move.
201+
202+
### Example Game Session
203+
204+
```
205+
=================================
206+
Welcome to Tic-Tac-Toe!
207+
=================================
208+
209+
You are X, AI is O
210+
Enter positions 1-9 as shown:
211+
212+
1 | 2 | 3
213+
-----------
214+
4 | 5 | 6
215+
-----------
216+
7 | 8 | 9
217+
218+
219+
| |
220+
-----------
221+
| |
222+
-----------
223+
| |
224+
225+
Your turn (X)
226+
Enter position (1-9): 5
227+
228+
| |
229+
-----------
230+
| X |
231+
-----------
232+
| |
233+
234+
AI is thinking... 🤔
235+
AI played position 1
236+
237+
O | |
238+
-----------
239+
| X |
240+
-----------
241+
| |
242+
243+
Your turn (X)
244+
Enter position (1-9): 3
245+
...
246+
```
247+
248+
### Running Tests
249+
250+
```bash
251+
# Run all tests
252+
cargo test
253+
254+
# Run tests with output
255+
cargo test -- --nocapture
256+
257+
# Run specific test
258+
cargo test test_ai_blocks_winning_move
259+
```
260+
261+
### Code Quality Checks
262+
263+
```bash
264+
# Check formatting
265+
cargo fmt --check
266+
267+
# Format code
268+
cargo fmt
269+
270+
# Run linter
271+
cargo clippy
272+
273+
# Build without warnings
274+
cargo build --release
275+
```
276+
277+
## Performance Considerations
278+
279+
### Minimax Optimization
280+
- The game tree is relatively small for Tic-Tac-Toe (maximum 9! = 362,880 possible games)
281+
- Depth-based scoring encourages faster wins, reducing average computation time
282+
- Early terminal state detection prunes unnecessary branches
283+
284+
### Memory Efficiency
285+
- Board uses a stack-allocated array instead of heap allocation
286+
- Game state cloning for AI simulations is lightweight (72 bytes)
287+
- No dynamic memory allocation during gameplay
288+
289+
## Future Enhancements
290+
291+
Potential improvements for future versions:
292+
293+
1. **Alpha-Beta Pruning**: Further optimize Minimax by skipping branches that cannot affect the final decision
294+
2. **Difficulty Levels**: Add options for easier AI by limiting search depth or introducing randomness
295+
3. **Undo/Redo**: Allow players to rewind moves
296+
4. **Game History**: Save and replay past games
297+
5. **GUI Version**: Create a graphical interface using a framework like `egui` or web-based UI
298+
6. **Network Play**: Enable human vs human over network
299+
7. **Different Board Sizes**: Generalize to NxN boards
300+
301+
## Conclusion
302+
303+
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.

topics/tic-tac-toe/src/main.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ fn main() {
2929
// Check game state
3030
match game.state() {
3131
GameState::Won(Player::Human) => {
32-
println!("🎉 Congratulations! You won!");
32+
println!("Congratulations! You won!");
3333
break;
3434
}
3535
GameState::Won(Player::AI) => {
36-
println!("💻 AI wins! Better luck next time!");
36+
println!("AI wins! Better luck next time!");
3737
break;
3838
}
3939
GameState::Draw => {
40-
println!("🤝 It's a draw! Well played!");
40+
println!("It's a draw! Well played!");
4141
break;
4242
}
4343
GameState::InProgress => {
@@ -52,18 +52,18 @@ fn main() {
5252
let position = get_human_move(&game);
5353

5454
if !game.make_move(position) {
55-
println!("Invalid move! Try again.");
55+
println!("Invalid move! Try again.");
5656
continue;
5757
}
5858
} else {
5959
// AI turn
60-
println!("AI is thinking... 🤔");
60+
println!("AI is thinking...");
6161

6262
if let Some(position) = ai.find_best_move(&game) {
6363
game.make_move(position);
6464
println!("AI played position {}", position + 1);
6565
} else {
66-
println!("Error: AI couldn't find a move!");
66+
println!("Error: AI couldn't find a move!");
6767
break;
6868
}
6969
}
@@ -93,11 +93,11 @@ fn get_human_move(game: &Game) -> usize {
9393
if game.available_moves().contains(&position) {
9494
return position;
9595
} else {
96-
println!("That position is already taken! Try another.");
96+
println!("That position is already taken! Try another.");
9797
}
9898
}
9999
_ => {
100-
println!("Invalid input! Please enter a number between 1 and 9.");
100+
println!("Invalid input! Please enter a number between 1 and 9.");
101101
}
102102
}
103103
}

0 commit comments

Comments
 (0)