diff --git a/tp3/README.md b/tp3/README.md
index 3cd003ecd11bc69a427fd599f37e3cd3f64710c7..29a10913f17d4205dccaafa1ec395ddbec622426 100644
--- a/tp3/README.md
+++ b/tp3/README.md
@@ -305,5 +305,54 @@ impl<L, R> Either<L, R> {
 ```
 ## 2.2. With trait bounds
 ```rs
+use std::io::{BufRead, BufReader, BufWriter, Read, Write};
+
+struct IOEncoder<E, I, O> {
+    encoder: E,
+    input: I,
+    output: O,
+}
+
+impl<E, I, O> IOEncoder<E, I, O>
+where
+    E: Encoder + Decoder,
+    I: Read,
+    O: Write,
+{
+    /// Creates a new IOEncoder
+    fn new(encoder: E, input: I, output: O) -> Self {
+        Self {
+            encoder,
+            input,
+            output,
+        }
+    }
+
+    /// Process this encoders input by encoding each line of the input and saving the encoded version to
+    /// the ouput
+    fn encode(&mut self) -> Result<(), std::io::Error> {
+        let reader = BufReader::new(&mut self.input);
+        let mut writer = BufWriter::new(&mut self.output);
+        for line in reader.lines() {
+            let line = line?;
+            let encoded = self.encoder.encode(&line);
+            writeln!(writer, "{}", encoded)?;
+        }
+        Ok(())
+    }
+
+    /// Process this encoders input by decoding each line of the input and saving the decoded version to
+    /// the ouput
+    fn decode(&mut self) -> Result<(), std::io::Error> {
+        let reader = BufReader::new(&mut self.input);
+        let mut writer = BufWriter::new(&mut self.output);
+        for line in reader.lines() {
+            let line = line?;
+            let decoded = self.encoder.decode(&line);
+            writeln!(writer, "{}", decoded)?;
+        }
+        Ok(())
+    }
+}
 ```
-# 3. Long exercise proposal
\ No newline at end of file
+# 3. Long exercise proposal
diff --git a/tp3/src/main.rs b/tp3/src/main.rs
index b3c38188684df729166a704e2d968ef514d1c290..5730ec99d100f448cf4174b83d0aa050a71dad60 100644
--- a/tp3/src/main.rs
+++ b/tp3/src/main.rs
@@ -331,7 +331,208 @@ where
 
 // =========================================================
 
-// =============== 2. Long exercise proposal ===============
+// =============== 3. Long exercise proposal ===============
+trait Game {
+    type Move;
+    type Player: std::fmt::Debug;
+
+    fn current_player(&self) -> Self::Player;
+    fn is_over(&self) -> bool;
+    fn make_move(&mut self, mv: Self::Move) -> Result<(), String>;
+    fn display(&self);
+    fn parse_move(input: &str) -> Result<Self::Move, String>;
+}
+
+// Tic Tac Toe
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum TicTacToePlayer {
+    X,
+    O,
+}
+
+struct TicTacToe {
+    board: [Option<TicTacToePlayer>; 9],
+    current_player: TicTacToePlayer,
+}
+
+impl TicTacToe {
+    fn new() -> Self {
+        Self {
+            board: [None; 9],
+            current_player: TicTacToePlayer::X,
+        }
+    }
+}
+
+impl Game for TicTacToe {
+    type Move = (usize, usize);
+    type Player = TicTacToePlayer;
+
+    fn current_player(&self) -> Self::Player {
+        self.current_player
+    }
+    fn is_over(&self) -> bool {
+        let b = &self.board;
+        // Check rows.
+        for i in 0..3 {
+            if let Some(p) = b[i * 3] {
+                if b[i * 3 + 1] == Some(p) && b[i * 3 + 2] == Some(p) {
+                    return true;
+                }
+            }
+        }
+        // Check columns.
+        for j in 0..3 {
+            if let Some(p) = b[j] {
+                if b[j + 3] == Some(p) && b[j + 6] == Some(p) {
+                    return true;
+                }
+            }
+        }
+        // Check diagonals.
+        if let Some(p) = b[0] {
+            if b[4] == Some(p) && b[8] == Some(p) {
+                return true;
+            }
+        }
+        if let Some(p) = b[2] {
+            if b[4] == Some(p) && b[6] == Some(p) {
+                return true;
+            }
+        }
+        // Game is over if board is full.
+        if b.iter().all(|&cell| cell.is_some()) {
+            return true;
+        }
+        false
+    }
+    fn make_move(&mut self, mv: Self::Move) -> Result<(), String> {
+        let (row, col) = mv;
+        if row >= 3 || col >= 3 {
+            return Err("Move out of bounds".to_string());
+        }
+        let idx = row * 3 + col;
+        if self.board[idx].is_some() {
+            return Err("Cell already occupied".to_string());
+        }
+        self.board[idx] = Some(self.current_player);
+        // Switch player.
+        self.current_player = match self.current_player {
+            TicTacToePlayer::X => TicTacToePlayer::O,
+            TicTacToePlayer::O => TicTacToePlayer::X,
+        };
+        Ok(())
+    }
+    fn display(&self) {
+        println!("Current board:");
+        for row in 0..3 {
+            for col in 0..3 {
+                let idx = row * 3 + col;
+                let symbol = match self.board[idx] {
+                    Some(TicTacToePlayer::X) => "X",
+                    Some(TicTacToePlayer::O) => "O",
+                    None => ".",
+                };
+                print!("{} ", symbol);
+            }
+            println!();
+        }
+    }
+    fn parse_move(input: &str) -> Result<Self::Move, String> {
+        // Expect two numbers separated by whitespace (e.g., "0 1")
+        let parts: Vec<&str> = input.split_whitespace().collect();
+        if parts.len() != 2 {
+            return Err("Please enter two numbers separated by space".to_string());
+        }
+        let row = parts[0]
+            .parse::<usize>()
+            .map_err(|_| "Invalid row".to_string())?;
+        let col = parts[1]
+            .parse::<usize>()
+            .map_err(|_| "Invalid column".to_string())?;
+        Ok((row, col))
+    }
+}
+
+// Jeu de Nim
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum NimGamePlayer {
+    First,
+    Second,
+}
+
+struct NimGame {
+    stones: u32,
+    current_player: NimGamePlayer,
+}
+
+impl NimGame {
+    fn new(stones: u32) -> Self {
+        Self {
+            stones: stones,
+            current_player: NimGamePlayer::First,
+        }
+    }
+}
+
+impl Game for NimGame {
+    type Move = u32;
+    type Player = NimGamePlayer;
+
+    fn current_player(&self) -> Self::Player {
+        self.current_player
+    }
+    fn is_over(&self) -> bool {
+        self.stones == 0
+    }
+    fn make_move(&mut self, mv: Self::Move) -> Result<(), String> {
+        if mv < 1 || mv > 2 {
+            return Err("You can only remove 1 or 2 stones".to_string());
+        }
+        if mv > self.stones {
+            return Err("Not enough stones remaining".to_string());
+        }
+        self.stones -= mv;
+        self.current_player = match self.current_player {
+            NimGamePlayer::First => NimGamePlayer::Second,
+            NimGamePlayer::Second => NimGamePlayer::First,
+        };
+        Ok(())
+    }
+    fn display(&self) {
+        println!("Stones remaining: {}", self.stones);
+    }
+    fn parse_move(input: &str) -> Result<Self::Move, String> {
+        input
+            .trim()
+            .parse::<u32>()
+            .map_err(|_| "Invalid number".to_string())
+    }
+}
+
+// Plays a game that implements the `Game` trait.
+fn play_game<G: Game>(mut game: G) {
+    while !game.is_over() {
+        game.display();
+        print!("Player {:?}, enter your move: ", game.current_player());
+        std::io::stdout().flush().unwrap();
+        let mut input = String::new();
+        std::io::stdin()
+            .read_line(&mut input)
+            .expect("Failed to read input");
+        match G::parse_move(input.trim()) {
+            Ok(mv) => {
+                if let Err(e) = game.make_move(mv) {
+                    println!("Error: {}", e);
+                }
+            }
+            Err(e) => println!("Invalid move: {}", e),
+        }
+    }
+    println!("Game over!");
+    game.display();
+}
+
 // =========================================================
 
 fn main() {
@@ -361,7 +562,7 @@ fn main() {
     );*/
 
     // 2.1. Without trait bounds
-    let want_a_string = true;
+    /*let want_a_string = true;
     let want_b_string = !want_a_string;
     let a = if want_a_string {
         Either::Left("A")
@@ -381,7 +582,31 @@ fn main() {
         println!("It is a number: {}", a.right().unwrap());
     }
     println!("{}", b.as_ref().right().unwrap());
-    println!("{}", b.left().unwrap_or("this is no string :/"));
+    println!("{}", b.left().unwrap_or("this is no string :/"));*/
+
+    // 3. Long exercise proposal
+    println!("\n--- Game engine demo ---");
+    let mut choice = String::new();
+    println!("\n--- Select game ---");
+    println!("1: Tic Tac Toe");
+    println!("2: Nim Game");
+    let mut game_choice = String::new();
+    std::io::stdin().read_line(&mut game_choice).unwrap();
+    match game_choice.trim() {
+        "1" => {
+            let game = TicTacToe::new();
+            play_game(game);
+        }
+        "2" => {
+            println!("Enter initial number of stones:");
+            let mut stones_str = String::new();
+            std::io::stdin().read_line(&mut stones_str).unwrap();
+            let stones = stones_str.trim().parse::<u32>().unwrap_or(10);
+            let game = NimGame::new(stones);
+            play_game(game);
+        }
+        _ => println!("Invalid game choice"),
+    }
 }
 
 // ====================== Tests ======================