From 7298a1528d5bbd5f960478729dd21df0f47e82da Mon Sep 17 00:00:00 2001 From: Samuel Ortiz Date: Fri, 12 Sep 2025 01:04:20 +0200 Subject: [PATCH 01/12] Project description Signed-off-by: Samuel Ortiz Signed-off-by: Proxyfil --- README.md | 73 ++++++++++++++++++++++++++ topics/p2p-transfer-protocol/README.md | 29 ++++++++++ topics/port-scanner/README.md | 25 +++++++++ topics/tic-tac-toe/README.md | 25 +++++++++ topics/web-scraper/README.md | 24 +++++++++ 5 files changed, 176 insertions(+) create mode 100644 README.md create mode 100644 topics/p2p-transfer-protocol/README.md create mode 100644 topics/port-scanner/README.md create mode 100644 topics/tic-tac-toe/README.md create mode 100644 topics/web-scraper/README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..96e246b --- /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 September 24th, 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/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/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*. From 259b7ac06c2af3a08c3e4052ee22f13c2890c51f Mon Sep 17 00:00:00 2001 From: Samuel Ortiz Date: Fri, 12 Sep 2025 23:38:04 +0200 Subject: [PATCH 02/12] READM: Deadline bump Signed-off-by: Samuel Ortiz Signed-off-by: Proxyfil --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96e246b..73204f0 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,4 @@ For example, a grade of `8/10` for a topic which grade factor is `1.1` will gene ## Deadline -All submitted PRs will be evaluated on September 24th, 2025 at 11:00 PM UTC. +All submitted PRs will be evaluated on October 30th, 2025 at 11:00 PM UTC. From afc1accc37ded0333d756f7553e6a88d01f6c135 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Sun, 28 Sep 2025 16:53:54 +0200 Subject: [PATCH 03/12] docs: Created architecture doc file Signed-off-by: Proxyfil --- topics/tic-tac-toe/docs/architecture.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 topics/tic-tac-toe/docs/architecture.md diff --git a/topics/tic-tac-toe/docs/architecture.md b/topics/tic-tac-toe/docs/architecture.md new file mode 100644 index 0000000..c59d6b4 --- /dev/null +++ b/topics/tic-tac-toe/docs/architecture.md @@ -0,0 +1,12 @@ +# A Simple Tic-Tac-Toe Agent +Implemented by Pierre-Louis Leclerc + +## Project description + +## Architecture Overview + +### Modules and Packages + +### Interactions Between Modules + +### Game loop \ No newline at end of file From 9f2b36a071cf413f007571a446727c579127b17c Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Sun, 28 Sep 2025 16:59:55 +0200 Subject: [PATCH 04/12] feat: Initiate cargo project Signed-off-by: Proxyfil --- topics/tic-tac-toe/Cargo.toml | 6 ++++++ topics/tic-tac-toe/src/main.rs | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 topics/tic-tac-toe/Cargo.toml create mode 100644 topics/tic-tac-toe/src/main.rs 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/src/main.rs b/topics/tic-tac-toe/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/topics/tic-tac-toe/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From cfc5df81e91d76958a07de85bd119cdd6bf17d4c Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Sun, 28 Sep 2025 17:21:44 +0200 Subject: [PATCH 05/12] feat(src): Created source code to create board and game objects Signed-off-by: Proxyfil --- topics/tic-tac-toe/src/game/board.rs | 23 +++++++++++++++++++++++ topics/tic-tac-toe/src/game/game.rs | 23 +++++++++++++++++++++++ topics/tic-tac-toe/src/game/mod.rs | 3 +++ topics/tic-tac-toe/src/game/robot.rs | 0 topics/tic-tac-toe/src/main.rs | 9 ++++++++- 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 topics/tic-tac-toe/src/game/board.rs create mode 100644 topics/tic-tac-toe/src/game/game.rs create mode 100644 topics/tic-tac-toe/src/game/mod.rs create mode 100644 topics/tic-tac-toe/src/game/robot.rs 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..ccf8b11 --- /dev/null +++ b/topics/tic-tac-toe/src/game/board.rs @@ -0,0 +1,23 @@ +pub struct Board { + cells: [[Option; 3]; 3] +} + +impl Board { + pub fn new() -> Self { + Board { + cells: [[None; 3]; 3] + } + } + + pub fn display(&self) { + for row in &self.cells { + for cell in row { + match cell { + Some(symbol) => print!("| {} |", symbol), + None => print!("| . |"), + } + } + println!(); + } + } +} \ 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..951ba98 --- /dev/null +++ b/topics/tic-tac-toe/src/game/game.rs @@ -0,0 +1,23 @@ +use crate::game::board::Board; + +pub struct Game { + board: Board +} + +impl Game { + pub fn new() -> Self { + println!("Starting a new game of Tic Tac Toe!"); + Game { + board: Board::new() + } + } + + pub fn play(&mut self) { + self.display_board(); + + } + + pub fn display_board(&self) { + self.board.display(); + } +} \ 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..e69de29 diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index e7a11a9..bfff42f 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -1,3 +1,10 @@ +mod game; + +use crate::game::game::Game; + fn main() { - println!("Hello, world!"); + println!("Starting Tic Tac Toe game..." ); + let mut game = Game::new(); + + game.play(); } From 1254b5fbafb09d46a35699641a4b797037441078 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:10:07 +0200 Subject: [PATCH 06/12] feat: Created gameloop to place X and O Signed-off-by: Proxyfil --- topics/tic-tac-toe/Cargo.lock | 7 ++++ topics/tic-tac-toe/src/game/board.rs | 26 ++++++++++++++- topics/tic-tac-toe/src/game/game.rs | 50 +++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 topics/tic-tac-toe/Cargo.lock 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/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs index ccf8b11..be01ef2 100644 --- a/topics/tic-tac-toe/src/game/board.rs +++ b/topics/tic-tac-toe/src/game/board.rs @@ -10,14 +10,38 @@ impl Board { } 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!("| . |"), + 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) + } } \ 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 index 951ba98..c0fa43e 100644 --- a/topics/tic-tac-toe/src/game/game.rs +++ b/topics/tic-tac-toe/src/game/game.rs @@ -1,23 +1,65 @@ use crate::game::board::Board; +use std::io; pub struct Game { - board: Board + board: Board, + current_player: char, } impl Game { pub fn new() -> Self { println!("Starting a new game of Tic Tac Toe!"); Game { - board: Board::new() + board: Board::new(), + current_player: 'X', } } pub fn play(&mut self) { - self.display_board(); - + loop { + self.display_board(); + println!("Player {}'s turn", self.current_player); + + if self.get_player_move() { + 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_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 From ce9aa35d9b472b7880d72ac7d5ddb6824e25c7d7 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:13:21 +0200 Subject: [PATCH 07/12] feat: Created draw detection when grid is full Signed-off-by: Proxyfil --- topics/tic-tac-toe/src/game/board.rs | 11 +++++++++++ topics/tic-tac-toe/src/game/game.rs | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/topics/tic-tac-toe/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs index be01ef2..7bd8d53 100644 --- a/topics/tic-tac-toe/src/game/board.rs +++ b/topics/tic-tac-toe/src/game/board.rs @@ -44,4 +44,15 @@ impl Board { 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 + } } \ 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 index c0fa43e..ec1a2fe 100644 --- a/topics/tic-tac-toe/src/game/game.rs +++ b/topics/tic-tac-toe/src/game/game.rs @@ -21,6 +21,12 @@ impl Game { println!("Player {}'s turn", self.current_player); if self.get_player_move() { + // 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(); } } From f6390f413b9697a04fdd1ca95d021aecb3f8efb9 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:15:27 +0200 Subject: [PATCH 08/12] feat: Added win detection Signed-off-by: Proxyfil --- topics/tic-tac-toe/src/game/board.rs | 37 ++++++++++++++++++++++++++++ topics/tic-tac-toe/src/game/game.rs | 7 ++++++ 2 files changed, 44 insertions(+) diff --git a/topics/tic-tac-toe/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs index 7bd8d53..349b12f 100644 --- a/topics/tic-tac-toe/src/game/board.rs +++ b/topics/tic-tac-toe/src/game/board.rs @@ -55,4 +55,41 @@ impl Board { } 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 + } } \ 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 index ec1a2fe..e4fd39f 100644 --- a/topics/tic-tac-toe/src/game/game.rs +++ b/topics/tic-tac-toe/src/game/game.rs @@ -21,6 +21,13 @@ impl Game { println!("Player {}'s turn", self.current_player); if self.get_player_move() { + // Check for winner after a successful move + if let Some(winner) = self.board.check_winner() { + self.display_board(); + println!("Game Over! Player {} wins!", winner); + break; + } + // Check for draw after a successful move if self.board.is_full() { self.display_board(); From 245007eb85f75fc5b8bfa1edd80629ffcc49d3c0 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:21:46 +0200 Subject: [PATCH 09/12] feat: Implemented robot to play randomly Signed-off-by: Proxyfil --- topics/tic-tac-toe/Cargo.lock | 126 +++++++++++++++++++++++++++ topics/tic-tac-toe/Cargo.toml | 1 + topics/tic-tac-toe/src/game/board.rs | 8 ++ topics/tic-tac-toe/src/game/game.rs | 33 ++++++- topics/tic-tac-toe/src/game/robot.rs | 50 +++++++++++ 5 files changed, 215 insertions(+), 3 deletions(-) diff --git a/topics/tic-tac-toe/Cargo.lock b/topics/tic-tac-toe/Cargo.lock index 32be7da..fe74594 100644 --- a/topics/tic-tac-toe/Cargo.lock +++ b/topics/tic-tac-toe/Cargo.lock @@ -2,6 +2,132 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[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.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +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 index a579fbf..f0d7fd0 100644 --- a/topics/tic-tac-toe/Cargo.toml +++ b/topics/tic-tac-toe/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +rand = "0.8" diff --git a/topics/tic-tac-toe/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs index 349b12f..a84c335 100644 --- a/topics/tic-tac-toe/src/game/board.rs +++ b/topics/tic-tac-toe/src/game/board.rs @@ -92,4 +92,12 @@ impl Board { 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 index e4fd39f..2d86b90 100644 --- a/topics/tic-tac-toe/src/game/game.rs +++ b/topics/tic-tac-toe/src/game/game.rs @@ -1,9 +1,12 @@ 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 { @@ -12,19 +15,34 @@ impl Game { Game { board: Board::new(), current_player: 'X', + robot: Robot::new('O'), + is_robot_game: true, // Set to true to play against robot } } pub fn play(&mut self) { loop { self.display_board(); - println!("Player {}'s turn", self.current_player); - if self.get_player_move() { + 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(); - println!("Game Over! Player {} wins!", winner); + if self.is_robot_game && winner == 'O' { + println!("Game Over! Robot wins!"); + } else { + println!("Game Over! Player {} wins!", winner); + } break; } @@ -58,6 +76,15 @@ impl Game { } } + 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(); diff --git a/topics/tic-tac-toe/src/game/robot.rs b/topics/tic-tac-toe/src/game/robot.rs index e69de29..f819a51 100644 --- a/topics/tic-tac-toe/src/game/robot.rs +++ b/topics/tic-tac-toe/src/game/robot.rs @@ -0,0 +1,50 @@ +use crate::game::board::Board; +use rand::Rng; + +pub struct Robot { + symbol: char, +} + +impl Robot { + pub fn new(symbol: char) -> Self { + Robot { symbol } + } + + pub fn make_move(&self, board: &mut Board) -> Option { + let available_positions = self.get_available_positions(board); + + if available_positions.is_empty() { + return None; + } + + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..available_positions.len()); + let chosen_position = available_positions[random_index]; + + if board.place_symbol_by_position(chosen_position, self.symbol) { + Some(chosen_position) + } else { + None + } + } + + fn get_available_positions(&self, board: &Board) -> Vec { + let mut available = Vec::new(); + + for position in 1..=9 { + let (row, col) = self.position_to_coords(position); + if board.is_position_empty(row, col) { + available.push(position); + } + } + + available + } + + 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 From 0052edacac1583f004640eea8c6a2a0fe3434339 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:27:34 +0200 Subject: [PATCH 10/12] feat: Implemented minimax algorithm for the robot with depth importance Signed-off-by: Proxyfil --- topics/tic-tac-toe/Cargo.lock | 126 --------------------------- topics/tic-tac-toe/Cargo.toml | 1 - topics/tic-tac-toe/src/game/board.rs | 1 + topics/tic-tac-toe/src/game/robot.rs | 95 ++++++++++++++++---- 4 files changed, 77 insertions(+), 146 deletions(-) diff --git a/topics/tic-tac-toe/Cargo.lock b/topics/tic-tac-toe/Cargo.lock index fe74594..32be7da 100644 --- a/topics/tic-tac-toe/Cargo.lock +++ b/topics/tic-tac-toe/Cargo.lock @@ -2,132 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[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.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[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.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -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 index f0d7fd0..a579fbf 100644 --- a/topics/tic-tac-toe/Cargo.toml +++ b/topics/tic-tac-toe/Cargo.toml @@ -4,4 +4,3 @@ version = "0.1.0" edition = "2024" [dependencies] -rand = "0.8" diff --git a/topics/tic-tac-toe/src/game/board.rs b/topics/tic-tac-toe/src/game/board.rs index a84c335..1079bb0 100644 --- a/topics/tic-tac-toe/src/game/board.rs +++ b/topics/tic-tac-toe/src/game/board.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct Board { cells: [[Option; 3]; 3] } diff --git a/topics/tic-tac-toe/src/game/robot.rs b/topics/tic-tac-toe/src/game/robot.rs index f819a51..0a1cab1 100644 --- a/topics/tic-tac-toe/src/game/robot.rs +++ b/topics/tic-tac-toe/src/game/robot.rs @@ -1,44 +1,101 @@ use crate::game::board::Board; -use rand::Rng; pub struct Robot { symbol: char, + opponent_symbol: char, } impl Robot { pub fn new(symbol: char) -> Self { - Robot { symbol } + let opponent_symbol = if symbol == 'X' { 'O' } else { 'X' }; + Robot { symbol, opponent_symbol } } pub fn make_move(&self, board: &mut Board) -> Option { - let available_positions = self.get_available_positions(board); + let best_position = self.find_best_move(board); - if available_positions.is_empty() { - return None; - } - - let mut rng = rand::thread_rng(); - let random_index = rng.gen_range(0..available_positions.len()); - let chosen_position = available_positions[random_index]; - - if board.place_symbol_by_position(chosen_position, self.symbol) { - Some(chosen_position) + if let Some(position) = best_position { + if board.place_symbol_by_position(position, self.symbol) { + Some(position) + } else { + None + } } else { None } } - fn get_available_positions(&self, board: &Board) -> Vec { - let mut available = Vec::new(); - + 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) { - available.push(position); + // 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); + } } } - - available + + 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) { From 1eb151a49234a89b33d541cba4891a12f6d0d37b Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:33:49 +0200 Subject: [PATCH 11/12] docs: Updated documentation to describe project Signed-off-by: Proxyfil --- topics/tic-tac-toe/docs/architecture.md | 83 +++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/topics/tic-tac-toe/docs/architecture.md b/topics/tic-tac-toe/docs/architecture.md index c59d6b4..49a74f8 100644 --- a/topics/tic-tac-toe/docs/architecture.md +++ b/topics/tic-tac-toe/docs/architecture.md @@ -1,12 +1,85 @@ # A Simple Tic-Tac-Toe Agent Implemented by Pierre-Louis Leclerc -## Project description +## Project Description -## Architecture Overview +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. -### Modules and Packages +### 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 -### Interactions Between Modules +## Components and Modules -### Game loop \ No newline at end of file +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 From ab612c7cb6660cb694a45cd03db91645b233ca72 Mon Sep 17 00:00:00 2001 From: Proxyfil Date: Wed, 1 Oct 2025 23:37:11 +0200 Subject: [PATCH 12/12] docs: Added some commentaries to code Signed-off-by: Proxyfil --- topics/tic-tac-toe/src/game/game.rs | 1 + topics/tic-tac-toe/src/main.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/topics/tic-tac-toe/src/game/game.rs b/topics/tic-tac-toe/src/game/game.rs index 2d86b90..2f32f00 100644 --- a/topics/tic-tac-toe/src/game/game.rs +++ b/topics/tic-tac-toe/src/game/game.rs @@ -20,6 +20,7 @@ impl Game { } } + // Main game loop pub fn play(&mut self) { loop { self.display_board(); diff --git a/topics/tic-tac-toe/src/main.rs b/topics/tic-tac-toe/src/main.rs index bfff42f..f2494c7 100644 --- a/topics/tic-tac-toe/src/main.rs +++ b/topics/tic-tac-toe/src/main.rs @@ -2,6 +2,7 @@ mod game; use crate::game::game::Game; +// Main game entry point fn main() { println!("Starting Tic Tac Toe game..." ); let mut game = Game::new();