diff --git a/README.md b/README.md new file mode 100644 index 0000000..73204f0 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Polytech DevOps - System Programming Project + +## Rules + +Students MUST pick one out of the [four proposed topics](topics). + +Before the project deadline, students MUST turn in an [architecture document](#architecture-and-documentation) and an [implementation](#implementation) for the selected topic, in the form of a [GitHub pull request](#project-contribution). + +## Topics + +Four different topics are proposed: + +1. [A networking port scanner](topics/port-scanner). +1. [A peer-to-peer file transfer protocol](topics/p2p-transfer-protocol). +1. [A web scraper](topics/web-scraper). +1. [A tic-tac-toe AI agent](topics/tic-tac-toe). + +## Grade + +Your work will be evaluated based on the following criteria: + +### Architecture and Documentation + +**This accounts for 40% of the final grade** + +You MUST provide a short, `markdown` formatted and English written document describing your project architecture. + +It MUST live under a projects top level folder called `docs/`, e.g. `docs/architecture.md`. + +It SHOULD at least contain the following sections: + +1. Project definition: What is it? What are the goals of the tool/project? +1. Components and modules: Describe which modules compose your project, and how they interact together. Briefly justify why you architectured it this way. +1. Usage: How can one use it? Give usage examples. + +### Implementation + +**This accounts for 40% of the final grade** + +The project MUST be implemented in Rust. + +The implementation MUST be formatted, build without warnings (including `clippy` warnings) and commented. + +The implementation modules and crates MAY be unit tested. + +### Project Contribution + +**This accounts for 20% of the final grade** + +The project MUST be submitted as one single GitHub pull request (PR) against the [current](https://github.com/dev-sys-do/project-2427) repository, for the selected project. + +For example, a student picking the `p2p-transfer-protocol` topic MUST send a PR that adds all deliverables (source code, documentation) to the `topics/p2p-transfer-protocol/` folder. + +All submitted PRs will not be evaluated until the project deadline. They can thus be incomplete, rebased, closed, and modified until the project deadline. + +A pull request quality is evaluated on the following criteria: +* Commit messages: Each git commit message should provide a clear description and explanation of what the corresponding change brings and does. +* History: The pull request git history MUST be linear (no merge points) and SHOULD represent the narrative of the underlying work. It is a representation of the author's logical work in putting the implementation together. + +A very good reference on the topic: https://github.blog/developer-skills/github/write-better-commits-build-better-projects/ + +### Grade Factor + +All proposed topics have a grade factor, describing their relative complexity. + +The final grade is normalized against the selected topic's grade factor: `final_grade = grade * topic_grade_factor`. + +For example, a grade of `8/10` for a topic which grade factor is `1.1` will generate a final grade of `8.8/10`. + + +## Deadline + +All submitted PRs will be evaluated on October 30th, 2025 at 11:00 PM UTC. diff --git a/topics/p2p-transfer-protocol/README.md b/topics/p2p-transfer-protocol/README.md new file mode 100644 index 0000000..8d57551 --- /dev/null +++ b/topics/p2p-transfer-protocol/README.md @@ -0,0 +1,29 @@ +# A P2P Transfer Protocol + +## Description and Goal + +Build a CLI tool that allows two users on the same network to transfer a single file to each other. +The tool should be able to act as both the sender and the receiver, without a central server. + +It is expected for a sender to know the IP of the receiver, i.e. there is no discovery protocol. + +```shell +# Receiving a file on port 9000 +p2p-tool listen --port 9000 --output ./shared + +# Sending a file +p2p-tool send --file report.pdf --to 192.168.1.100 --port 9000 +``` + +## Hints and Suggestions + +- Define and document a simple networking protocol with a few commands. For example + - HELLO: For the sender to offer a file to the receiver. It takes a file size argument. + - ACK: For the receiver to tell the sender it is ready to receive a proposed file. + - NACK: For the receiver to reject a proposed file. + - SEND: Send, for the sender to actually send a file. It also takes a file size argument, that must match the `HELLO` offer. +- Start a receiving thread for every sender connection. + +## Grade Factor + +The grade factor for this project is *1*. diff --git a/topics/port-scanner/README.md b/topics/port-scanner/README.md new file mode 100644 index 0000000..a2c94e4 --- /dev/null +++ b/topics/port-scanner/README.md @@ -0,0 +1,25 @@ +# Network Port Scanner + +## Description and Goal + +Build a *multi-threaded* command-line application that scans a range of ports at a URL or IP to check if they are open. + +The tool displays all open ports at the target URL. + +```shell +Usage: scanner [OPTIONS] URL/IP + +Options: + -p, --ports TCP or UDP port ranges. Can be set multiple times. + -t, --threads Max number of threads. + -h, --help Print help (see more with '--help') + -V, --version Print version +``` + +## References + +[nmap](https://nmap.org/) + +## Grade Factor + +The grade factor for this project is *0.9*. 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..a579fbf --- /dev/null +++ b/topics/tic-tac-toe/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "tic-tac-toe" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/topics/tic-tac-toe/README.md b/topics/tic-tac-toe/README.md new file mode 100644 index 0000000..357e40e --- /dev/null +++ b/topics/tic-tac-toe/README.md @@ -0,0 +1,25 @@ +# A Simple Tic-Tac-Toe Agent + +## Description and Goal + +The goal is to build a command-line [Tic-Tac-Toe](https://en.wikipedia.org/wiki/Tic-tac-toe) game where a human player can play against an AI opponent. + +The AI should be "smart" enough to play optimally, meaning it can't be beaten and will either win or draw every game. + +## Game State Representation + +A simple 2D array or a 1D array of a fixed size is a good choice. + +## Agent Algorithm + +The recommended choice for the Agent algorithm is the `Minimax` algorithm with a depth-first search: The AI assumes its opponent will always make the best possible move, and it will choose its own move to minimize the maximum possible loss (or, conversely, maximize the minimum possible gain). + +A simpler, alternative option is the tree search one: The algorithm builds a game tree to explore all possible future moves. The agent assigns scores to the board's end states (+1 for an AI win, -1 for a human win, 0 for a draw) and "propagates" those scores up the tree to find the best move. + +## References + +[Minimax and tic-tac-toe](https://www.neverstopbuilding.com/blog/minimax) + +## Grade Factor + +The grade factor for this project is *1.2*. diff --git a/topics/tic-tac-toe/docs/architecture.md b/topics/tic-tac-toe/docs/architecture.md new file mode 100644 index 0000000..49a74f8 --- /dev/null +++ b/topics/tic-tac-toe/docs/architecture.md @@ -0,0 +1,85 @@ +# A Simple Tic-Tac-Toe Agent +Implemented by Pierre-Louis Leclerc + +## Project Description + +This project is a command-line Tic-Tac-Toe game implemented in Rust that features an intelligent AI opponent using the minimax algorithm. The game allows a human player to compete against a perfect-playing robot in a traditional 3x3 grid tic-tac-toe match. + +### Goals +- **Perfect AI Implementation**: Create an unbeatable AI opponent using the minimax algorithm +- **Clean Architecture**: Demonstrate modular design with separation of concerns +- **Interactive Gameplay**: Provide an engaging command-line interface for human players +- **Educational Value**: Showcase game theory concepts and AI decision-making algorithms + +## Components and Modules + +The project is structured using a modular architecture with clear separation of responsibilities: + +### Core Modules + +#### 1. **Board Module** (`src/game/board.rs`) +- **Purpose**: Manages the game state and board operations +- **Key Features**: + - 3x3 grid representation using `[[Option; 3]; 3]` + - Position-based input system (1-9 numbering) + - Win condition detection (rows, columns, diagonals) + - Draw detection (full board check) + - Move validation and symbol placement +- **Justification**: Encapsulates all board-related logic, making it easy to test and maintain game state + +#### 2. **Robot Module** (`src/game/robot.rs`) +- **Purpose**: Implements the AI opponent using minimax algorithm +- **Key Features**: + - Perfect play strategy through minimax decision tree + - Recursive game state evaluation + - Optimal move selection with depth consideration + - Strategic scoring system (wins, losses, draws) +- **Justification**: Separates AI logic from game flow, allowing for easy algorithm swapping or improvements + +#### 3. **Game Module** (`src/game/game.rs`) +- **Purpose**: Orchestrates the overall game flow and user interaction +- **Key Features**: + - Turn-based game loop management + - Human vs Robot player coordination + - Input handling and validation + - Game state transitions and end conditions +- **Justification**: Acts as the controller, coordinating between board state and AI decisions + +#### 4. **Main Module** (`src/main.rs`) +- **Purpose**: Entry point and game initialization +- **Key Features**: + - Game instance creation + - Application startup +- **Justification**: Keeps the entry point minimal and focused + +### Module Interactions + +``` +main.rs + ↓ creates +Game + ↓ manages +Board ←→ Robot + ↑ ↓ + └─ reads state + makes moves +``` + +- **Game** coordinates between human input and robot decisions +- **Board** maintains authoritative game state for both players +- **Robot** analyzes board state to make optimal moves + +## Usage + +### Building and Running + +```bash +# Navigate to the project directory +cd topics/tic-tac-toe + +# Build the project +cargo build + +# Run the game +cargo run +``` \ No newline at end of file diff --git a/topics/tic-tac-toe/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs new file mode 100644 index 0000000..1079bb0 --- /dev/null +++ b/topics/tic-tac-toe/src/game/board.rs @@ -0,0 +1,104 @@ +#[derive(Clone)] +pub struct Board { + cells: [[Option; 3]; 3] +} + +impl Board { + pub fn new() -> Self { + Board { + cells: [[None; 3]; 3] + } + } + + pub fn display(&self) { + println!("Choose a position (1-9):"); + let mut position = 1; + for row in &self.cells { + for cell in row { + match cell { + Some(symbol) => print!("| {} |", symbol), + None => print!("| {} |", position), + } + position += 1; + } + println!(); + } + } + + pub fn place_symbol_by_position(&mut self, position: usize, symbol: char) -> bool { + if position < 1 || position > 9 { + return false; + } + + let (row, col) = self.position_to_coords(position); + if self.cells[row][col].is_none() { + self.cells[row][col] = Some(symbol); + true + } else { + false + } + } + + fn position_to_coords(&self, position: usize) -> (usize, usize) { + let index = position - 1; // Convert to 0-based index + let row = index / 3; + let col = index % 3; + (row, col) + } + + pub fn is_full(&self) -> bool { + for row in &self.cells { + for cell in row { + if cell.is_none() { + return false; + } + } + } + true + } + + pub fn check_winner(&self) -> Option { + // Check rows + for row in &self.cells { + if let Some(symbol) = row[0] { + if row[1] == Some(symbol) && row[2] == Some(symbol) { + return Some(symbol); + } + } + } + + // Check columns + for col in 0..3 { + if let Some(symbol) = self.cells[0][col] { + if self.cells[1][col] == Some(symbol) && self.cells[2][col] == Some(symbol) { + return Some(symbol); + } + } + } + + // Check diagonals + // Top-left to bottom-right + if let Some(symbol) = self.cells[0][0] { + if self.cells[1][1] == Some(symbol) && self.cells[2][2] == Some(symbol) { + return Some(symbol); + } + } + + // Top-right to bottom-left + if let Some(symbol) = self.cells[0][2] { + if self.cells[1][1] == Some(symbol) && self.cells[2][0] == Some(symbol) { + return Some(symbol); + } + } + + None + } + + pub fn is_position_empty(&self, row: usize, col: usize) -> bool { + if row < 3 && col < 3 { + self.cells[row][col].is_none() + } else { + false + } + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/game/game.rs b/topics/tic-tac-toe/src/game/game.rs new file mode 100644 index 0000000..2f32f00 --- /dev/null +++ b/topics/tic-tac-toe/src/game/game.rs @@ -0,0 +1,106 @@ +use crate::game::board::Board; +use crate::game::robot::Robot; +use std::io; + +pub struct Game { + board: Board, + current_player: char, + robot: Robot, + is_robot_game: bool, +} + +impl Game { + pub fn new() -> Self { + println!("Starting a new game of Tic Tac Toe!"); + Game { + board: Board::new(), + current_player: 'X', + robot: Robot::new('O'), + is_robot_game: true, // Set to true to play against robot + } + } + + // Main game loop + pub fn play(&mut self) { + loop { + self.display_board(); + + let move_successful = if self.is_robot_game && self.current_player == 'O' { + // Robot's turn + println!("Robot's turn (O)"); + self.get_robot_move() + } else { + // Human player's turn + println!("Player {}'s turn", self.current_player); + self.get_player_move() + }; + + if move_successful { + // Check for winner after a successful move + if let Some(winner) = self.board.check_winner() { + self.display_board(); + if self.is_robot_game && winner == 'O' { + println!("Game Over! Robot wins!"); + } else { + println!("Game Over! Player {} wins!", winner); + } + break; + } + + // Check for draw after a successful move + if self.board.is_full() { + self.display_board(); + println!("Game Over! It's a draw!"); + break; + } + self.switch_player(); + } + } + } + + pub fn display_board(&self) { + println!(); + self.board.display(); + println!(); + } + + fn get_player_move(&mut self) -> bool { + println!("Enter position (1-9): "); + let position = self.get_position_input(); + + if self.board.place_symbol_by_position(position, self.current_player) { + println!("Move placed successfully!"); + true + } else { + println!("Invalid move! Position is either occupied or invalid. Try again."); + false + } + } + + fn get_robot_move(&mut self) -> bool { + if let Some(position) = self.robot.make_move(&mut self.board) { + println!("Robot chose position {}", position); + true + } else { + false + } + } + + fn get_position_input(&self) -> usize { + loop { + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + + match input.trim().parse::() { + Ok(num) if num >= 1 && num <= 9 => return num, + _ => println!("Please enter a number between 1 and 9:"), + } + } + } + + fn switch_player(&mut self) { + self.current_player = if self.current_player == 'X' { 'O' } else { 'X' }; + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/game/mod.rs b/topics/tic-tac-toe/src/game/mod.rs new file mode 100644 index 0000000..5887e93 --- /dev/null +++ b/topics/tic-tac-toe/src/game/mod.rs @@ -0,0 +1,3 @@ +pub mod board; +pub mod game; +pub mod robot; \ No newline at end of file diff --git a/topics/tic-tac-toe/src/game/robot.rs b/topics/tic-tac-toe/src/game/robot.rs new file mode 100644 index 0000000..0a1cab1 --- /dev/null +++ b/topics/tic-tac-toe/src/game/robot.rs @@ -0,0 +1,107 @@ +use crate::game::board::Board; + +pub struct Robot { + symbol: char, + opponent_symbol: char, +} + +impl Robot { + pub fn new(symbol: char) -> Self { + let opponent_symbol = if symbol == 'X' { 'O' } else { 'X' }; + Robot { symbol, opponent_symbol } + } + + pub fn make_move(&self, board: &mut Board) -> Option { + let best_position = self.find_best_move(board); + + if let Some(position) = best_position { + if board.place_symbol_by_position(position, self.symbol) { + Some(position) + } else { + None + } + } else { + None + } + } + + fn find_best_move(&self, board: &Board) -> Option { + let mut best_score = i32::MIN; + let mut best_position = None; + + for position in 1..=9 { + let (row, col) = self.position_to_coords(position); + if board.is_position_empty(row, col) { + // Create a copy of the board to test the move + let mut test_board = board.clone(); + test_board.place_symbol_by_position(position, self.symbol); + + // Calculate the score using minimax + let score = self.minimax(&test_board, 0, false); + + if score > best_score { + best_score = score; + best_position = Some(position); + } + } + } + + best_position + } + + fn minimax(&self, board: &Board, depth: i32, is_maximizing: bool) -> i32 { + // Check terminal states + if let Some(winner) = board.check_winner() { + if winner == self.symbol { + return 10 - depth; // Robot wins (prefer shorter paths to victory) + } else { + return depth - 10; // Opponent wins (prefer longer paths to defeat) + } + } + + if board.is_full() { + return 0; // Draw + } + + if is_maximizing { + // Robot's turn - maximize score + let mut max_score = i32::MIN; + + for position in 1..=9 { + let (row, col) = self.position_to_coords(position); + if board.is_position_empty(row, col) { + let mut test_board = board.clone(); + test_board.place_symbol_by_position(position, self.symbol); + + let score = self.minimax(&test_board, depth + 1, false); + max_score = max_score.max(score); + } + } + + max_score + } else { + // Opponent's turn - minimize score + let mut min_score = i32::MAX; + + for position in 1..=9 { + let (row, col) = self.position_to_coords(position); + if board.is_position_empty(row, col) { + let mut test_board = board.clone(); + test_board.place_symbol_by_position(position, self.opponent_symbol); + + let score = self.minimax(&test_board, depth + 1, true); + min_score = min_score.min(score); + } + } + + min_score + } + } + + fn position_to_coords(&self, position: usize) -> (usize, usize) { + let index = position - 1; // Convert to 0-based index + let row = index / 3; + let col = index % 3; + (row, col) + } +} \ No newline at end of file diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..f2494c7 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,11 @@ +mod game; + +use crate::game::game::Game; + +// Main game entry point +fn main() { + println!("Starting Tic Tac Toe game..." ); + let mut game = Game::new(); + + game.play(); +} diff --git a/topics/web-scraper/README.md b/topics/web-scraper/README.md new file mode 100644 index 0000000..09a3b23 --- /dev/null +++ b/topics/web-scraper/README.md @@ -0,0 +1,24 @@ +# A Web Scraper + +## Description and Goal + +Build a command-line application that can download and process web pages from a given list of starting URLs. + +The tool should use multiple threads to scrape pages concurrently, following links it finds along the way, up to a certain depth or page count. + +All scraped pages should be stored locally, in the same hierarchical order they were scraped: if page `A` points to page `B`, page `B` must be stored under a `B` folder located where page `A` is stored: + +```sheell +output/ + |- A.html + |- B/ + |--- B.html +``` + +```shell +webcrawl --output ./crawled_url --depth 10 +``` + +## Grade Factor + +The grade factor for this project is *1.1*.