Repository: thomas-mauran/chess-tui (language: rust)

Issue:
Online multiplayer
## Problem

The goal here would be to be able to do a 2 player match over internet.

## Solution

Some other tui games are using techniques to achieve this such as using ssh like in [this project](https://github.com/ricott1/sshattrick)



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index eb0f17a..20b7fa7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -70,9 +70,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.4.1"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
 [[package]]
 name = "cassowary"
@@ -172,7 +172,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
 dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.6.0",
  "crossterm_winapi",
  "mio",
  "parking_lot",
@@ -242,9 +242,9 @@ checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
 
 [[package]]
 name = "getrandom"
-version = "0.2.11"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
@@ -317,7 +317,7 @@ version = "0.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
 dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.6.0",
  "libc",
  "redox_syscall",
 ]
@@ -403,18 +403,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.70"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.33"
+version = "1.0.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
 dependencies = [
  "proc-macro2",
 ]
@@ -425,7 +425,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
 dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.6.0",
  "cassowary",
  "compact_str",
  "crossterm",
@@ -466,7 +466,7 @@ version = "0.38.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
 dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -583,9 +583,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.39"
+version = "2.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
+checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -623,9 +623,9 @@ dependencies = [
 
 [[package]]
 name = "uci"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6b9b0b262ac8343865c678e09dc793f4065e1359507608a1fe1d2c34426c40"
+checksum = "4c1b9eaa4d073139668463f22d8f5151f53600a10909b9902ffe7547b176044a"
 dependencies = [
  "log",
 ]
@@ -708,7 +708,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.0",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -728,17 +728,18 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.0",
- "windows_aarch64_msvc 0.52.0",
- "windows_i686_gnu 0.52.0",
- "windows_i686_msvc 0.52.0",
- "windows_x86_64_gnu 0.52.0",
- "windows_x86_64_gnullvm 0.52.0",
- "windows_x86_64_msvc 0.52.0",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -749,9 +750,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -761,9 +762,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -773,9 +774,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -785,9 +792,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -797,9 +804,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -809,9 +816,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -821,6 +828,6 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.0"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index c2333cb..2db239b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ repository = "https://github.com/thomas-mauran/chess-tui"
 clap = { version = "4.4.11", features = ["derive"] }
 dirs = "5.0.1"
 ratatui = "0.28.1"
-uci = "0.2.0"
+uci = "0.2.1"
 toml = "0.5.8"
 
 [features]
diff --git a/README.md b/README.md
index 74aa659..59e544a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 <div align="center">
 <h1>chess-tui</h1>
-A chess TUI implementation in rust 🦀
+A rusty chess game in your terminal 🦀
 
 ![board](./examples/play_against_white_bot.gif)
 
@@ -10,6 +10,10 @@ A chess TUI implementation in rust 🦀
   </div>
 </div>
 
+### Description
+
+Chess-tui is a simple chess game you can play from your terminal. It supports local 2 players mode, online multiplayer and playing against any UCI compatible chess engine.
+
 ### Quick install
 
 ```bash
@@ -30,7 +34,11 @@ If you want to install the game with your favorite package manager, you can find
 </details>
 <details>
   <summary>Local 2 player mode</summary>
-  <img src="./examples/demo.gif" alt="Helper menu" />
+  <img src="./examples/demo.gif" alt="Local 2 players" />
+</details>
+<details>
+  <summary>Online multiplayer</summary>
+  <img src="./website/static/gif/multiplayer.gif" alt="Online multiplayer" />
 </details>
 <details>
   <summary>Draws</summary>
diff --git a/examples/demo-two-player.gif b/examples/demo-two-player.gif
index e3c4036..6b3f556 100644
Binary files a/examples/demo-two-player.gif and b/examples/demo-two-player.gif differ
diff --git a/examples/demo-two-player.tape b/examples/demo-two-player.tape
index 2a7f999..cc9dc83 100644
--- a/examples/demo-two-player.tape
+++ b/examples/demo-two-player.tape
@@ -15,61 +15,62 @@ Set WindowBarSize 40
 Type "cargo run" Sleep 500ms  Enter
 
 Sleep 0.5s
-Down @0.8s
-Down @0.8s
-Down @0.8s
-Down @0.8s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Down @0.3s
+Down @0.3s
+Down @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 1.5s
 
-Down @0.8s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 
 Sleep 0.8s
-Left @0.8s
-Space @0.8s
+Left @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 Sleep 0.8s
 
-Right @0.8s
-Right @0.8s
-Right @0.8s
-Space @0.8s
+Right @0.3s
+Right @0.3s
+Right @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 Sleep 0.8s
 
-Left @0.8s
-Space @0.8s
+Left @0.3s
+Space @0.3s
 Sleep 0.8s
-Right @0.8s
-Right @0.8s
-Space @0.8s
+Right @0.3s
+Right @0.3s
+Space @0.3s
 Sleep 0.8s
 
-Up @0.8s
-Right @0.8s
-Right @0.8s
-Space @0.8s
+Up @0.3s
+Right @0.3s
+Right @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 Sleep 0.8s
 
 
-Up @0.8s
-Up @0.8s
-Up @0.8s
-Space @0.8s
+Up @0.3s
+Up @0.3s
+Up @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 Sleep 0.8s
diff --git a/examples/helper.gif b/examples/helper.gif
index e02ffe2..895c2e4 100644
Binary files a/examples/helper.gif and b/examples/helper.gif differ
diff --git a/examples/helper.tape b/examples/helper.tape
index 23351d7..5936e7d 100644
--- a/examples/helper.tape
+++ b/examples/helper.tape
@@ -18,6 +18,7 @@ Sleep 2s
 Down @0.3s
 Down @0.3s
 Down @0.3s
+Down @0.3s
 Space @0.3s
 Sleep 1s
 
diff --git a/examples/play_against_black_bot.gif b/examples/play_against_black_bot.gif
index 2b67df2..32d30a9 100644
Binary files a/examples/play_against_black_bot.gif and b/examples/play_against_black_bot.gif differ
diff --git a/examples/play_against_black_bot.tape b/examples/play_against_black_bot.tape
index 0658fa3..1050c80 100644
--- a/examples/play_against_black_bot.tape
+++ b/examples/play_against_black_bot.tape
@@ -15,37 +15,38 @@ Set WindowBarSize 40
 Type "cargo run" Sleep 500ms  Enter
 
 Sleep 2s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 1s
 
-Right @0.8s
-Space @0.8s
+Right @0.3s
+Space @0.3s
 Sleep 0.3s
 
-Down @0.8s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 0.3s
-Space @0.8s
+Space @0.3s
 
-Right @0.8s
-Space @0.8s
+Right @0.3s
+Space @0.3s
 Sleep 0.3s
-Space @0.8s
+Space @0.3s
 
-Right @0.8s
-Space @0.8s
+Right @0.3s
+Space @0.3s
 Sleep 0.3s
-Space @0.8s
-
-Left @0.8s
-Left @0.8s
-Left @0.8s
-Left @0.8s
-Left @0.8s
-Space @0.8s
+Space @0.3s
+
+Left @0.3s
+Left @0.3s
+Left @0.3s
+Left @0.3s
+Left @0.3s
+Space @0.3s
 Sleep 0.8s
-Space @0.8s
+Space @0.3s
 Sleep 0.8s
 
diff --git a/examples/play_against_white_bot.gif b/examples/play_against_white_bot.gif
index 73626a6..0fb7307 100644
Binary files a/examples/play_against_white_bot.gif and b/examples/play_against_white_bot.gif differ
diff --git a/examples/play_against_white_bot.tape b/examples/play_against_white_bot.tape
index ee9f940..f04b160 100644
--- a/examples/play_against_white_bot.tape
+++ b/examples/play_against_white_bot.tape
@@ -15,40 +15,41 @@ Set WindowBarSize 40
 Type "cargo run" Sleep 500ms  Enter
 
 Sleep 2s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 0.5s
-Space @0.8s
+Space @0.3s
 Sleep 0.5s
 
-Down @0.8s
-Down @0.8s
-Space @0.8s
-Space @0.8s
-
-Down @0.8s
-Space @0.8s
-Space @0.8s
-
-Up @0.8s
-Space @0.8s
-Space @0.8s
-
-Up @0.8s
-Left @0.8s
-Space @0.8s
-Space @0.8s
-
-Up @0.8s
-Left @0.8s
-Space @0.8s
-Right @0.8s
-Space @0.8s
-
-Left @0.8s
-Down @0.8s
-Space @0.8s
+Down @0.3s
+Down @0.3s
+Space @0.3s
+Space @0.3s
+
+Down @0.3s
+Space @0.3s
+Space @0.3s
+
+Up @0.3s
+Space @0.3s
+Space @0.3s
+
+Up @0.3s
+Left @0.3s
+Space @0.3s
+Space @0.3s
+
+Up @0.3s
+Left @0.3s
+Space @0.3s
+Right @0.3s
+Space @0.3s
+
+Left @0.3s
+Down @0.3s
+Space @0.3s
 Sleep 0.5s
-Space @0.8s
+Space @0.3s
 
 Sleep 5s
\ No newline at end of file
diff --git a/src/app.rs b/src/app.rs
index 63d5302..d1ae04a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2,14 +2,18 @@ use dirs::home_dir;
 use toml::Value;
 
 use crate::{
-    constants::{DisplayMode, Pages},
-    game_logic::{bot::Bot, game::Game},
+    constants::{DisplayMode, Pages, Popups},
+    game_logic::{bot::Bot, game::Game, opponent::Opponent},
     pieces::PieceColor,
+    server::game_server::GameServer,
 };
 use std::{
     error,
     fs::{self, File},
     io::Write,
+    net::{IpAddr, UdpSocket},
+    thread::sleep,
+    time::Duration,
 };
 
 /// Application result type.
@@ -23,12 +27,14 @@ pub struct App {
     pub game: Game,
     /// Current page to render
     pub current_page: Pages,
-    /// Used to show the help popup during the game or in the home menu
-    pub show_help_popup: bool,
-    /// Used to show the side selection popup when playing against the bot
-    pub show_color_popup: bool,
+    /// Current popup to render
+    pub current_popup: Option<Popups>,
     // Selected color when playing against the bot
     pub selected_color: Option<PieceColor>,
+    /// Hosting
+    pub hosting: Option<bool>,
+    /// Host Ip
+    pub host_ip: Option<String>,
     /// menu current cursor
     pub menu_cursor: u8,
     /// path of the chess engine
@@ -41,9 +47,10 @@ impl Default for App {
             running: true,
             game: Game::default(),
             current_page: Pages::Home,
-            show_help_popup: false,
-            show_color_popup: false,
+            current_popup: None,
             selected_color: None,
+            hosting: None,
+            host_ip: None,
             menu_cursor: 0,
             chess_engine_path: None,
         }
@@ -52,7 +59,11 @@ impl Default for App {
 
 impl App {
     pub fn toggle_help_popup(&mut self) {
-        self.show_help_popup = !self.show_help_popup;
+        if self.current_popup == Some(Popups::Help) {
+            self.current_popup = None;
+        } else {
+            self.current_popup = Some(Popups::Help);
+        }
     }
     pub fn toggle_credit_popup(&mut self) {
         if self.current_page == Pages::Home {
@@ -62,11 +73,64 @@ impl App {
         }
     }
 
+    pub fn setup_game_server(&mut self, host_color: PieceColor) {
+        let is_host_white = host_color == PieceColor::White;
+
+        std::thread::spawn(move || {
+            let game_server = GameServer::new(is_host_white);
+            game_server.run();
+        });
+
+        sleep(Duration::from_millis(100));
+    }
+
+    pub fn create_opponent(&mut self) {
+        let other_player_color = if self.selected_color.is_some() {
+            Some(self.selected_color.unwrap().opposite())
+        } else {
+            None
+        };
+        if self.hosting.unwrap() {
+            self.current_popup = Some(Popups::WaitingForOpponentToJoin);
+            self.host_ip = Some(format!("{}:2308", self.get_host_ip()));
+        }
+
+        let addr = self.host_ip.as_ref().unwrap().to_string();
+        let addr_with_port = addr.to_string();
+
+        // ping the server to see if it's up
+
+        let s = UdpSocket::bind(addr_with_port.clone());
+        if s.is_err() {
+            eprintln!("\nServer is unreachable. Make sure you entered the correct IP and port.");
+            self.host_ip = None;
+            return;
+        }
+
+        self.game.opponent = Some(Opponent::new(addr_with_port, other_player_color));
+
+        if !self.hosting.unwrap() {
+            // If we are not hosting (joining) we set the selected color as the opposite of the opposite player color
+            self.selected_color = Some(self.game.opponent.as_mut().unwrap().color.opposite());
+            self.game.opponent.as_mut().unwrap().game_started = true;
+        }
+        if self.selected_color.unwrap() == PieceColor::Black {
+            self.game.game_board.flip_the_board();
+        }
+    }
+
     pub fn go_to_home(&mut self) {
         self.current_page = Pages::Home;
         self.restart();
     }
 
+    pub fn get_host_ip(&self) -> IpAddr {
+        let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
+        socket.connect("8.8.8.8:80").unwrap(); // Use an external IP to identify the default route
+
+        socket.local_addr().unwrap().ip()
+    }
+
     /// Handles the tick event of the terminal.
     pub fn tick(&self) {}
 
@@ -105,15 +169,16 @@ impl App {
     }
 
     pub fn color_selection(&mut self) {
-        self.show_color_popup = false;
-
+        self.current_popup = None;
         let color = match self.menu_cursor {
             0 => PieceColor::White,
             1 => PieceColor::Black,
             _ => unreachable!("Invalid color selection"),
         };
         self.selected_color = Some(color);
+    }
 
+    pub fn bot_setup(&mut self) {
         let empty = "".to_string();
         let path = match self.chess_engine_path.as_ref() {
             Some(engine_path) => engine_path,
@@ -131,10 +196,21 @@ impl App {
         }
     }
 
+    pub fn hosting_selection(&mut self) {
+        let choice = self.menu_cursor == 0;
+        self.hosting = Some(choice);
+        self.current_popup = None;
+    }
+
     pub fn restart(&mut self) {
         let bot = self.game.bot.clone();
+        let opponent = self.game.opponent.clone();
         self.game = Game::default();
+
         self.game.bot = bot;
+        self.game.opponent = opponent;
+        self.current_popup = None;
+
         if self.game.bot.as_ref().is_some()
             && self
                 .game
@@ -152,17 +228,21 @@ impl App {
             0 => self.current_page = Pages::Solo,
             1 => {
                 self.menu_cursor = 0;
-                self.current_page = Pages::Bot
+                self.current_page = Pages::Multiplayer
             }
             2 => {
+                self.menu_cursor = 0;
+                self.current_page = Pages::Bot
+            }
+            3 => {
                 self.game.ui.display_mode = match self.game.ui.display_mode {
                     DisplayMode::ASCII => DisplayMode::DEFAULT,
                     DisplayMode::DEFAULT => DisplayMode::ASCII,
                 };
                 self.update_config();
             }
-            3 => self.show_help_popup = true,
-            4 => self.current_page = Pages::Credit,
+            4 => self.toggle_help_popup(),
+            5 => self.current_page = Pages::Credit,
             _ => {}
         }
     }
@@ -187,4 +267,14 @@ impl App {
         let mut file = File::create(config_path.clone()).unwrap();
         file.write_all(config.to_string().as_bytes()).unwrap();
     }
+
+    pub fn reset(&mut self) {
+        self.game = Game::default();
+        self.current_popup = None;
+        self.selected_color = None;
+        self.hosting = None;
+        self.host_ip = None;
+        self.menu_cursor = 0;
+        self.chess_engine_path = None;
+    }
 }
diff --git a/src/constants.rs b/src/constants.rs
index 30938c8..2cda12d 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -38,15 +38,26 @@ pub fn home_dir() -> Result<PathBuf, &'static str> {
     }
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum Pages {
     Home,
     Solo,
+    Multiplayer,
     Bot,
     Credit,
 }
 impl Pages {
     pub fn variant_count() -> usize {
-        5
+        6
     }
 }
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Popups {
+    ColorSelection,
+    MultiplayerSelection,
+    EnterHostIP,
+    WaitingForOpponentToJoin,
+    EnginePathError,
+    Help,
+}
diff --git a/src/game_logic/game.rs b/src/game_logic/game.rs
index 35c307e..f5ca10b 100644
--- a/src/game_logic/game.rs
+++ b/src/game_logic/game.rs
@@ -1,4 +1,4 @@
-use super::{bot::Bot, coord::Coord, game_board::GameBoard, ui::UI};
+use super::{bot::Bot, coord::Coord, game_board::GameBoard, opponent::Opponent, ui::UI};
 use crate::{
     pieces::{PieceColor, PieceMove, PieceType},
     utils::get_int_from_char,
@@ -12,7 +12,6 @@ pub enum GameState {
     Promotion,
 }
 
-#[derive(Clone)]
 pub struct Game {
     /// The GameBoard storing data about the board related stuff
     pub game_board: GameBoard,
@@ -20,18 +19,41 @@ pub struct Game {
     pub ui: UI,
     /// The struct to handle Bot related stuff
     pub bot: Option<Bot>,
+    /// The other player when playing in multiplayer
+    pub opponent: Option<Opponent>,
     /// Which player is it to play
     pub player_turn: PieceColor,
     /// The current state of the game (Playing, Draw, Checkmate. Promotion)
     pub game_state: GameState,
 }
 
+impl Clone for Game {
+    fn clone(&self) -> Self {
+        let opponent_clone = self.opponent.as_ref().map(|p| Opponent {
+            stream: p.stream.as_ref().and_then(|s| s.try_clone().ok()),
+            opponent_will_move: p.opponent_will_move,
+            color: p.color,
+            game_started: p.game_started,
+        });
+
+        Game {
+            game_board: self.game_board.clone(),
+            ui: self.ui.clone(),
+            bot: self.bot.clone(),
+            opponent: opponent_clone,
+            player_turn: self.player_turn,
+            game_state: self.game_state,
+        }
+    }
+}
+
 impl Default for Game {
     fn default() -> Self {
         Self {
             game_board: GameBoard::default(),
             ui: UI::default(),
             bot: None,
+            opponent: None,
             player_turn: PieceColor::White,
             game_state: GameState::Playing,
         }
@@ -45,6 +67,7 @@ impl Game {
             game_board,
             ui: UI::default(),
             bot: None,
+            opponent: None,
             player_turn,
             game_state: GameState::Playing,
         }
@@ -69,88 +92,135 @@ impl Game {
     }
 
     // Methods to select a cell on the board
-    // TODO: Split this in multiple methods
-    pub fn select_cell(&mut self) {
+    pub fn handle_cell_click(&mut self) {
         // If we are doing a promotion the cursor is used for the popup
         if self.game_state == GameState::Promotion {
-            self.promote_piece();
+            self.handle_promotion();
         } else if !(self.game_state == GameState::Checkmate)
             && !(self.game_state == GameState::Draw)
         {
             if self.ui.is_cell_selected() {
-                // We already selected a piece so we apply the move
-                if self.ui.cursor_coordinates.is_valid() {
-                    let selected_coords_usize = &self.ui.selected_coordinates.clone();
-                    let cursor_coords_usize = &self.ui.cursor_coordinates.clone();
-                    self.execute_move(selected_coords_usize, cursor_coords_usize);
-                    self.ui.unselect_cell();
-                    self.switch_player_turn();
+                self.already_selected_cell_action();
+            } else {
+                self.select_cell()
+            }
+        }
+        self.update_game_state();
+    }
 
-                    if self.game_board.is_draw(self.player_turn) {
-                        self.game_state = GameState::Draw;
+    fn update_game_state(&mut self) {
+        if self.game_board.is_checkmate(self.player_turn) {
+            self.game_state = GameState::Checkmate;
+        } else if self.game_board.is_draw(self.player_turn) {
+            self.game_state = GameState::Draw;
+        } else if self.game_board.is_latest_move_promotion() {
+            self.game_state = GameState::Promotion;
+        }
+    }
+
+    pub fn handle_promotion(&mut self) {
+        self.promote_piece();
+
+        if self.opponent.is_some() {
+            self.handle_multiplayer_promotion();
+        }
+
+        if self.bot.is_some() {
+            self.execute_bot_move();
+        }
+    }
+    pub fn already_selected_cell_action(&mut self) {
+        // We already selected a piece so we apply the move
+        if self.ui.cursor_coordinates.is_valid() {
+            let selected_coords_usize = &self.ui.selected_coordinates.clone();
+            let cursor_coords_usize = &self.ui.cursor_coordinates.clone();
+            self.execute_move(selected_coords_usize, cursor_coords_usize);
+            self.ui.unselect_cell();
+            self.switch_player_turn();
+
+            if self.game_board.is_draw(self.player_turn) {
+                self.game_state = GameState::Draw;
+            }
+
+            if (self.bot.is_none() || (self.bot.as_ref().map_or(false, |bot| bot.is_bot_starting)))
+                && (self.opponent.is_none())
+                && (!self.game_board.is_latest_move_promotion()
+                    || self.game_board.is_draw(self.player_turn)
+                    || self.game_board.is_checkmate(self.player_turn))
+            {
+                self.game_board.flip_the_board();
+            }
+
+            // If we play against a bot we will play his move and switch the player turn again
+            if self.bot.is_some() {
+                // do this in background
+                if self.game_board.is_latest_move_promotion() {
+                    self.game_state = GameState::Promotion;
+                }
+
+                if !(self.game_state == GameState::Promotion) {
+                    if self.game_board.is_checkmate(self.player_turn) {
+                        self.game_state = GameState::Checkmate;
                     }
 
-                    if (self.bot.is_none()
-                        || (self.bot.as_ref().map_or(false, |bot| bot.is_bot_starting)))
-                        && (!self.game_board.is_latest_move_promotion()
-                            || self.game_board.is_draw(self.player_turn)
-                            || self.game_board.is_checkmate(self.player_turn))
-                    {
-                        self.game_board.flip_the_board();
+                    if self.game_board.is_draw(self.player_turn) {
+                        self.game_state = GameState::Draw;
                     }
-                    // If we play against a bot we will play his move and switch the player turn again
-                    if self.bot.is_some() {
-                        // do this in background
-                        if self.game_board.is_latest_move_promotion() {
-                            self.game_state = GameState::Promotion;
-                        }
 
-                        if !(self.game_state == GameState::Promotion) {
-                            if self.game_board.is_checkmate(self.player_turn) {
-                                self.game_state = GameState::Checkmate;
-                            }
-                            if self.game_board.is_latest_move_promotion() {
-                                self.game_state = GameState::Promotion;
-                            }
-                            if !(self.game_state == GameState::Checkmate) {
-                                if let Some(bot) = self.bot.as_mut() {
-                                    bot.bot_will_move = true;
-                                }
-                            }
+                    if !(self.game_state == GameState::Checkmate) {
+                        if let Some(bot) = self.bot.as_mut() {
+                            bot.bot_will_move = true;
                         }
                     }
                 }
-            } else {
-                // Check if the piece on the cell can move before selecting it
-                let authorized_positions = self
-                    .game_board
-                    .get_authorized_positions(self.player_turn, self.ui.cursor_coordinates);
+            }
+            // If we play against a player we will wait for his move
+            if self.opponent.is_some() {
+                if self.game_board.is_latest_move_promotion() {
+                    self.game_state = GameState::Promotion;
+                } else {
+                    if self.game_board.is_checkmate(self.player_turn) {
+                        self.game_state = GameState::Checkmate;
+                    }
 
-                if authorized_positions.is_empty() {
-                    return;
-                }
-                if let Some(piece_color) =
-                    self.game_board.get_piece_color(&self.ui.cursor_coordinates)
-                {
-                    let authorized_positions = self
-                        .game_board
-                        .get_authorized_positions(self.player_turn, self.ui.cursor_coordinates);
-
-                    if piece_color == self.player_turn {
-                        self.ui.selected_coordinates = self.ui.cursor_coordinates;
-                        self.ui.old_cursor_position = self.ui.cursor_coordinates;
-                        self.ui
-                            .move_selected_piece_cursor(true, 1, authorized_positions);
+                    if self.game_board.is_draw(self.player_turn) {
+                        self.game_state = GameState::Draw;
                     }
+
+                    if !(self.game_state == GameState::Checkmate) {
+                        if let Some(opponent) = self.opponent.as_mut() {
+                            opponent.opponent_will_move = true;
+                        }
+                    }
+                    self.opponent
+                        .as_mut()
+                        .unwrap()
+                        .send_move_to_server(self.game_board.move_history.last().unwrap(), None);
                 }
             }
         }
-        if self.game_board.is_checkmate(self.player_turn) {
-            self.game_state = GameState::Checkmate;
-        } else if self.game_board.is_draw(self.player_turn) {
-            self.game_state = GameState::Draw;
-        } else if self.game_board.is_latest_move_promotion() {
-            self.game_state = GameState::Promotion;
+    }
+
+    pub fn select_cell(&mut self) {
+        // Check if the piece on the cell can move before selecting it
+        let authorized_positions = self
+            .game_board
+            .get_authorized_positions(self.player_turn, self.ui.cursor_coordinates);
+
+        if authorized_positions.is_empty() {
+            return;
+        }
+        if let Some(piece_color) = self.game_board.get_piece_color(&self.ui.cursor_coordinates) {
+            let authorized_positions = self
+                .game_board
+                .get_authorized_positions(self.player_turn, self.ui.cursor_coordinates);
+
+            if piece_color == self.player_turn {
+                self.ui.selected_coordinates = self.ui.cursor_coordinates;
+                self.ui.old_cursor_position = self.ui.cursor_coordinates;
+                self.ui
+                    .move_selected_piece_cursor(true, 1, authorized_positions);
+            }
         }
     }
 
@@ -222,11 +292,19 @@ impl Game {
                 self.game_board.board[last_move.to.row as usize][last_move.to.col as usize] =
                     Some((new_piece, piece_color));
             }
+
+            // We replace the piece type in the move history
+            let latest_move = self.game_board.move_history.last_mut().unwrap();
+            latest_move.piece_type = new_piece;
+            self.game_board.board_history.pop();
+            self.game_board.board_history.push(self.game_board.board);
         }
         self.game_state = GameState::Playing;
         self.ui.promotion_cursor = 0;
         if !self.game_board.is_draw(self.player_turn)
             && !self.game_board.is_checkmate(self.player_turn)
+            && self.opponent.is_none()
+            && self.bot.is_none()
         {
             self.game_board.flip_the_board();
         }
@@ -314,4 +392,53 @@ impl Game {
         // We store the current position of the board
         self.game_board.board_history.push(self.game_board.board);
     }
+
+    pub fn execute_opponent_move(&mut self) {
+        let opponent_move = self.opponent.as_mut().unwrap().read_stream();
+        self.game_board.flip_the_board();
+        self.opponent.as_mut().unwrap().opponent_will_move = false;
+
+        if opponent_move.is_empty() {
+            return;
+        }
+
+        let from_y = get_int_from_char(opponent_move.chars().next());
+        let from_x = get_int_from_char(opponent_move.chars().nth(1));
+        let to_y = get_int_from_char(opponent_move.chars().nth(2));
+        let to_x = get_int_from_char(opponent_move.chars().nth(3));
+
+        let mut promotion_piece: Option<PieceType> = None;
+        if opponent_move.chars().count() == 5 {
+            promotion_piece = match opponent_move.chars().nth(4) {
+                Some('q') => Some(PieceType::Queen),
+                Some('r') => Some(PieceType::Rook),
+                Some('b') => Some(PieceType::Bishop),
+                Some('n') => Some(PieceType::Knight),
+                _ => None,
+            };
+        }
+
+        let from = &Coord::new(from_y, from_x);
+        let to = &Coord::new(to_y, to_x);
+
+        self.execute_move(from, to);
+
+        if promotion_piece.is_some() {
+            self.game_board.board[to_y as usize][to_x as usize] =
+                Some((promotion_piece.unwrap(), self.player_turn));
+        }
+        self.game_board.flip_the_board();
+    }
+
+    pub fn handle_multiplayer_promotion(&mut self) {
+        let opponent = self.opponent.as_mut().unwrap();
+
+        let last_move_promotion_type = self.game_board.get_last_move_piece_type_as_string();
+
+        opponent.send_move_to_server(
+            self.game_board.move_history.last().unwrap(),
+            Some(last_move_promotion_type),
+        );
+        opponent.opponent_will_move = true;
+    }
 }
diff --git a/src/game_logic/game_board.rs b/src/game_logic/game_board.rs
index c42e2c8..574da7f 100644
--- a/src/game_logic/game_board.rs
+++ b/src/game_logic/game_board.rs
@@ -78,6 +78,20 @@ impl GameBoard {
         }
     }
 
+    pub fn get_last_move_piece_type_as_string(&self) -> String {
+        if let Some(last_move) = self.move_history.last() {
+            match last_move.piece_type {
+                PieceType::Pawn => return String::from("p"),
+                PieceType::Rook => return String::from("r"),
+                PieceType::Knight => return String::from("n"),
+                PieceType::Bishop => return String::from("b"),
+                PieceType::Queen => return String::from("q"),
+                PieceType::King => return String::from("k"),
+            }
+        }
+        String::from("")
+    }
+
     pub fn increment_consecutive_non_pawn_or_capture(
         &mut self,
         piece_type_from: PieceType,
@@ -143,6 +157,11 @@ impl GameBoard {
             self.get_piece_type(&coordinates),
             self.get_piece_color(&coordinates),
         ) {
+            // If the piece color is not the same as the player turn we return an empty vector it's not his turn
+            if player_turn != piece_color {
+                return vec![];
+            }
+
             piece_type.authorized_positions(
                 &coordinates,
                 piece_color,
diff --git a/src/game_logic/mod.rs b/src/game_logic/mod.rs
index 67aa5d8..750d2cb 100644
--- a/src/game_logic/mod.rs
+++ b/src/game_logic/mod.rs
@@ -3,4 +3,5 @@ pub mod bot;
 pub mod coord;
 pub mod game;
 pub mod game_board;
+pub mod opponent;
 pub mod ui;
diff --git a/src/game_logic/opponent.rs b/src/game_logic/opponent.rs
new file mode 100644
index 0000000..8ab4272
--- /dev/null
+++ b/src/game_logic/opponent.rs
@@ -0,0 +1,183 @@
+use crate::pieces::{PieceColor, PieceMove};
+use std::{
+    io::{Read, Write},
+    net::TcpStream,
+    panic,
+};
+
+pub struct Opponent {
+    // The stream to communicate with the engine
+    pub stream: Option<TcpStream>,
+    /// Used to indicate if a Opponent move is following
+    pub opponent_will_move: bool,
+    // The color of the Opponent
+    pub color: PieceColor,
+    /// Is Game started
+    pub game_started: bool,
+}
+
+// Custom Default implementation
+impl Default for Opponent {
+    fn default() -> Self {
+        Opponent {
+            stream: None,
+            opponent_will_move: false,
+            color: PieceColor::Black,
+            game_started: false,
+        }
+    }
+}
+
+impl Clone for Opponent {
+    fn clone(&self) -> Self {
+        Opponent {
+            stream: self.stream.as_ref().and_then(|s| s.try_clone().ok()), // Custom handling for TcpStream
+            opponent_will_move: self.opponent_will_move,
+            color: self.color,
+            game_started: self.game_started,
+        }
+    }
+}
+
+impl Opponent {
+    pub fn copy(&self) -> Self {
+        Opponent {
+            stream: None,
+            opponent_will_move: self.opponent_will_move,
+            color: self.color,
+            game_started: self.game_started,
+        }
+    }
+
+    pub fn new(addr: String, color: Option<PieceColor>) -> Opponent {
+        // Attempt to connect 5 times to the provided address
+        let mut stream: Option<TcpStream> = None; // Initialize `stream` as None
+        for _ in 0..5 {
+            match TcpStream::connect(addr.clone()) {
+                Ok(s) => {
+                    stream = Some(s);
+                    break;
+                }
+                Err(_) => {
+                    println!(
+                        "Failed to connect to the server addr: {}. Retrying...",
+                        addr
+                    );
+                }
+            }
+        }
+
+        if let Some(stream) = stream {
+            // Determine the Opponent's color
+            let color = match color {
+                Some(color) => color, // Use the provided color if available
+                None => get_color_from_stream(&stream),
+            };
+
+            let opponent_will_move = match color {
+                PieceColor::White => true,
+                PieceColor::Black => false,
+            };
+
+            Opponent {
+                stream: Some(stream),
+                opponent_will_move,
+                color,
+                game_started: false,
+            }
+        } else {
+            panic!(
+                "Failed to connect to the server after 5 attempts to the following address {}",
+                addr
+            );
+        }
+    }
+
+    pub fn start_stream(&mut self, addr: &str) {
+        match TcpStream::connect(addr) {
+            Ok(stream) => {
+                self.stream = Some(stream);
+            }
+            Err(e) => {
+                panic!("Failed to connect: {}", e);
+            }
+        }
+    }
+
+    pub fn send_end_game_to_server(&mut self) {
+        if let Some(game_stream) = self.stream.as_mut() {
+            if let Err(e) = game_stream.write_all("ended".as_bytes()) {
+                eprintln!("Failed to send end game: {}", e);
+            }
+        }
+    }
+
+    pub fn send_move_to_server(
+        &mut self,
+        move_to_send: &PieceMove,
+        promotion_type: Option<String>,
+    ) {
+        if let Some(game_stream) = self.stream.as_mut() {
+            let move_str = format!(
+                "{}{}{}{}{}",
+                move_to_send.from.row,
+                move_to_send.from.col,
+                move_to_send.to.row,
+                move_to_send.to.col,
+                match promotion_type {
+                    Some(promotion) => promotion,
+                    None => "".to_string(),
+                }
+            );
+            if let Err(e) = game_stream.write_all(move_str.as_bytes()) {
+                eprintln!("Failed to send move: {}", e);
+            }
+        }
+    }
+
+    pub fn read_stream(&mut self) -> String {
+        if let Some(game_stream) = self.stream.as_mut() {
+            let mut buffer = vec![0; 5];
+            let buf = game_stream.read(&mut buffer);
+            match buf {
+                Ok(bytes_read) => {
+                    let response = String::from_utf8_lossy(&buffer[..bytes_read]);
+
+                    if response.trim() == "ended" || response.trim() == "" {
+                        panic!("Game ended by the other Opponent");
+                    }
+                    response.to_string()
+                }
+                Err(e) => {
+                    eprintln!("Failed to read from stream: {}", e);
+                    "".to_string()
+                }
+            }
+        } else {
+            "".to_string()
+        }
+    }
+}
+
+pub fn get_color_from_stream(mut stream: &TcpStream) -> PieceColor {
+    let mut buffer = [0; 5];
+    let bytes_read = stream.read(&mut buffer).unwrap(); // Number of bytes read
+    let color = String::from_utf8_lossy(&buffer[..bytes_read]).to_string();
+
+    match color.as_str() {
+        "w" => PieceColor::White,
+        "b" => PieceColor::Black,
+        _ => panic!("Failed to get color from stream"),
+    }
+}
+
+pub fn wait_for_game_start(mut stream: &TcpStream) {
+    let mut buffer = [0; 5];
+    let bytes_read = stream.read(&mut buffer).unwrap(); // Number of bytes read
+    let response = String::from_utf8_lossy(&buffer[..bytes_read]).to_string();
+
+    match response.as_str() {
+        "s" => (),
+        _ => panic!("Failed to get color from stream"),
+    }
+}
diff --git a/src/game_logic/ui.rs b/src/game_logic/ui.rs
index ebb388f..08d4ce6 100644
--- a/src/game_logic/ui.rs
+++ b/src/game_logic/ui.rs
@@ -2,7 +2,7 @@ use super::{coord::Coord, game::Game};
 use crate::{
     constants::{DisplayMode, BLACK, UNDEFINED_POSITION, WHITE},
     pieces::{PieceColor, PieceMove, PieceType},
-    ui::main_ui::render_cell,
+    ui::{main_ui::render_cell, prompt::Prompt},
     utils::{convert_position_into_notation, get_cell_paragraph, invert_position},
 };
 use ratatui::{
@@ -35,6 +35,8 @@ pub struct UI {
     pub mouse_used: bool,
     /// The skin of the game
     pub display_mode: DisplayMode,
+    // The prompt for the player
+    pub prompt: Prompt,
 }
 
 impl Default for UI {
@@ -51,6 +53,7 @@ impl Default for UI {
             height: 0,
             mouse_used: false,
             display_mode: DisplayMode::DEFAULT,
+            prompt: Prompt::new(),
         }
     }
 }
@@ -320,7 +323,7 @@ impl UI {
     }
 
     /// Method to render the board
-    pub fn board_render(&mut self, area: Rect, frame: &mut Frame, game: &Game) {
+    pub fn board_render(&mut self, area: Rect, frame: &mut Frame<'_>, game: &Game) {
         let width = area.width / 8;
         let height = area.height / 8;
         let border_height = area.height / 2 - (4 * height);
@@ -390,6 +393,14 @@ impl UI {
                         last_move_from = invert_position(&last_move.map(|m| m.from).unwrap());
                         last_move_to = invert_position(&last_move.map(|m| m.to).unwrap());
                     }
+
+                    // If the opponent is the same as the last move player, we don't want to show his last move
+                    if game.opponent.is_some()
+                        && game.opponent.as_ref().unwrap().color == game.player_turn
+                    {
+                        last_move_from = Coord::undefined();
+                        last_move_to = Coord::undefined();
+                    }
                 }
 
                 let mut positions: Vec<Coord> = vec![];
diff --git a/src/handler.rs b/src/handler.rs
index bfa5888..b61db8a 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -1,3 +1,4 @@
+use crate::constants::Popups;
 use crate::game_logic::coord::Coord;
 use crate::game_logic::game::GameState;
 use crate::{
@@ -25,130 +26,230 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
         }
     }
 
-    match key_event.code {
-        // Exit application on `q`
-        KeyCode::Char('q') => {
-            app.quit();
+    if app.current_popup == Some(Popups::EnterHostIP) {
+        if key_event.kind == KeyEventKind::Press {
+            match key_event.code {
+                KeyCode::Enter => {
+                    app.game.ui.prompt.submit_message();
+                    if app.current_page == Pages::Multiplayer {
+                        app.host_ip = Some(app.game.ui.prompt.message.clone());
+                    }
+                    app.current_popup = None;
+                }
+                KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
+                KeyCode::Backspace => app.game.ui.prompt.delete_char(),
+                KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
+                KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
+                KeyCode::Esc => {
+                    app.current_popup = None;
+                    if app.current_page == Pages::Multiplayer {
+                        app.hosting = None;
+                        app.selected_color = None;
+                        app.menu_cursor = 0;
+                    }
+                    app.current_page = Pages::Home;
+                }
+                _ => {}
+            }
         }
-        // Exit application on `Ctrl-C`
-        KeyCode::Char('c' | 'C') => {
-            if key_event.modifiers == KeyModifiers::CONTROL {
+    } else {
+        match key_event.code {
+            // Exit application on `q`
+            KeyCode::Char('q') => {
                 app.quit();
             }
-        }
-        // Counter handlers
-        KeyCode::Right | KeyCode::Char('l') => {
-            // When we are in the color selection menu
-            if app.current_page == Pages::Bot && app.selected_color.is_none() {
-                app.menu_cursor_right(2);
-            } else if app.game.game_state == GameState::Promotion {
-                app.game.ui.cursor_right_promotion();
-            } else if !(app.game.game_state == GameState::Checkmate)
-                && !(app.game.game_state == GameState::Draw)
-            {
-                let authorized_positions = app.game.game_board.get_authorized_positions(
-                    app.game.player_turn,
-                    app.game.ui.selected_coordinates,
-                );
-                app.game.ui.cursor_right(authorized_positions);
+            // Exit application on `Ctrl-C`
+            KeyCode::Char('c' | 'C') => {
+                if key_event.modifiers == KeyModifiers::CONTROL {
+                    app.quit();
+                }
+            }
+            // Counter handlers
+            // Counter handlers
+            KeyCode::Right | KeyCode::Char('l') => {
+                if (app.current_page == Pages::Multiplayer
+                    && (app.hosting.is_none() || app.selected_color.is_none()))
+                    || (app.current_page == Pages::Bot && app.selected_color.is_none())
+                {
+                    app.menu_cursor_right(2);
+                } else if app.game.game_state == GameState::Promotion {
+                    app.game.ui.cursor_right_promotion();
+                } else if !(app.game.game_state == GameState::Checkmate)
+                    && !(app.game.game_state == GameState::Draw)
+                {
+                    let authorized_positions = app.game.game_board.get_authorized_positions(
+                        app.game.player_turn,
+                        app.game.ui.selected_coordinates,
+                    );
+                    app.game.ui.cursor_right(authorized_positions);
+                }
             }
-        }
-        KeyCode::Left | KeyCode::Char('h') => {
-            // When we are in the color selection menu
-            if app.current_page == Pages::Bot && app.selected_color.is_none() {
-                app.menu_cursor_left(2);
-            } else if app.game.game_state == GameState::Promotion {
-                app.game.ui.cursor_left_promotion();
-            } else if !(app.game.game_state == GameState::Checkmate)
-                && !(app.game.game_state == GameState::Draw)
-            {
-                let authorized_positions = app.game.game_board.get_authorized_positions(
-                    app.game.player_turn,
-                    app.game.ui.selected_coordinates,
-                );
 
-                app.game.ui.cursor_left(authorized_positions);
+            KeyCode::Left | KeyCode::Char('h') => {
+                if (app.current_page == Pages::Multiplayer
+                    && (app.hosting.is_none() || app.selected_color.is_none()))
+                    || (app.current_page == Pages::Bot && app.selected_color.is_none())
+                {
+                    app.menu_cursor_left(2);
+                } else if app.game.game_state == GameState::Promotion {
+                    app.game.ui.cursor_left_promotion();
+                } else if !(app.game.game_state == GameState::Checkmate)
+                    && !(app.game.game_state == GameState::Draw)
+                {
+                    let authorized_positions = app.game.game_board.get_authorized_positions(
+                        app.game.player_turn,
+                        app.game.ui.selected_coordinates,
+                    );
+
+                    app.game.ui.cursor_left(authorized_positions);
+                }
             }
-        }
-        KeyCode::Up | KeyCode::Char('k') => {
-            if app.current_page == Pages::Home {
-                app.menu_cursor_up(Pages::variant_count() as u8);
-            } else if !(app.game.game_state == GameState::Checkmate)
-                && !(app.game.game_state == GameState::Draw)
-                && !(app.game.game_state == GameState::Promotion)
-            {
-                let authorized_positions = app.game.game_board.get_authorized_positions(
-                    app.game.player_turn,
-                    app.game.ui.selected_coordinates,
-                );
-                app.game.ui.cursor_up(authorized_positions);
+            KeyCode::Up | KeyCode::Char('k') => {
+                if app.current_page == Pages::Home {
+                    app.menu_cursor_up(Pages::variant_count() as u8);
+                } else if !(app.game.game_state == GameState::Checkmate)
+                    && !(app.game.game_state == GameState::Draw)
+                    && !(app.game.game_state == GameState::Promotion)
+                {
+                    let authorized_positions = app.game.game_board.get_authorized_positions(
+                        app.game.player_turn,
+                        app.game.ui.selected_coordinates,
+                    );
+                    app.game.ui.cursor_up(authorized_positions);
+                }
             }
-        }
-        KeyCode::Down | KeyCode::Char('j') => {
-            if app.current_page == Pages::Home {
-                app.menu_cursor_down(Pages::variant_count() as u8);
-            } else if !(app.game.game_state == GameState::Checkmate)
-                && !(app.game.game_state == GameState::Draw)
-                && !(app.game.game_state == GameState::Promotion)
-            {
-                let authorized_positions = app.game.game_board.get_authorized_positions(
-                    app.game.player_turn,
-                    app.game.ui.selected_coordinates,
-                );
+            KeyCode::Down | KeyCode::Char('j') => {
+                if app.current_page == Pages::Home {
+                    app.menu_cursor_down(Pages::variant_count() as u8);
+                } else if !(app.game.game_state == GameState::Checkmate)
+                    && !(app.game.game_state == GameState::Draw)
+                    && !(app.game.game_state == GameState::Promotion)
+                {
+                    let authorized_positions = app.game.game_board.get_authorized_positions(
+                        app.game.player_turn,
+                        app.game.ui.selected_coordinates,
+                    );
 
-                app.game.ui.cursor_down(authorized_positions);
+                    app.game.ui.cursor_down(authorized_positions);
+                }
             }
-        }
-        KeyCode::Char(' ') | KeyCode::Enter => {
-            if app.current_page == Pages::Bot && app.selected_color.is_none() {
-                app.color_selection();
-            } else if app.current_page == Pages::Home {
-                app.menu_select();
-            } else {
-                app.game.select_cell();
+            KeyCode::Char(' ') | KeyCode::Enter => match app.current_page {
+                Pages::Home => {
+                    app.menu_select();
+                }
+                Pages::Bot => {
+                    if app.selected_color.is_none() {
+                        app.color_selection();
+                        app.bot_setup();
+                    } else {
+                        app.game.handle_cell_click();
+                    }
+                }
+                Pages::Multiplayer => {
+                    if app.hosting.is_none() {
+                        app.hosting_selection();
+                    } else if app.selected_color.is_none() {
+                        if app.hosting.is_some() && app.hosting.unwrap() {
+                            app.color_selection();
+                        }
+                    } else {
+                        app.game.handle_cell_click();
+                    }
+                }
+                Pages::Credit => {
+                    app.current_page = Pages::Home;
+                }
+                _ => {
+                    app.game.handle_cell_click();
+                }
+            },
+            KeyCode::Char('?') => {
+                if app.current_page != Pages::Credit {
+                    app.toggle_help_popup();
+                }
             }
-        }
-        KeyCode::Char('?') => {
-            if app.current_page != Pages::Credit {
-                app.toggle_help_popup();
+            KeyCode::Char('r') => {
+                // We can't restart the game if it's a multiplayer one
+                if app.game.opponent.is_none() {
+                    app.restart();
+                }
             }
-        }
-        KeyCode::Char('r') => app.restart(),
-        KeyCode::Esc => {
-            if app.show_help_popup {
-                app.show_help_popup = false;
-            } else if app.show_color_popup {
-                app.show_color_popup = false;
-                app.current_page = Pages::Home;
-            } else if app.current_page == Pages::Credit {
-                app.current_page = Pages::Home;
-            } else if app.current_page == Pages::Bot && app.selected_color.is_none() {
-                app.current_page = Pages::Home;
-                app.show_color_popup = false;
-                app.menu_cursor = 0;
+            KeyCode::Esc => {
+                match app.current_popup {
+                    Some(Popups::ColorSelection) => {
+                        app.current_popup = None;
+                        app.selected_color = None;
+                        app.hosting = None;
+                        app.current_page = Pages::Home;
+                        app.menu_cursor = 0;
+                    }
+                    Some(Popups::MultiplayerSelection) => {
+                        app.current_popup = None;
+                        app.selected_color = None;
+                        app.hosting = None;
+                        app.current_page = Pages::Home;
+                        app.menu_cursor = 0;
+                    }
+                    Some(Popups::WaitingForOpponentToJoin) => {
+                        app.current_popup = None;
+                        app.selected_color = None;
+                        app.hosting = None;
+                        app.current_page = Pages::Home;
+                        app.menu_cursor = 0;
+                    }
+                    Some(Popups::Help) => {
+                        app.current_popup = None;
+                    }
+                    _ => {}
+                }
+
+                match app.current_page {
+                    Pages::Bot => {
+                        app.current_page = Pages::Home;
+                        app.menu_cursor = 0;
+                        app.selected_color = None;
+                    }
+                    Pages::Credit => {
+                        app.current_page = Pages::Home;
+                    }
+                    _ => {}
+                }
+
+                app.game.ui.unselect_cell();
             }
-            app.game.ui.unselect_cell();
-        }
-        KeyCode::Char('b') => {
-            let display_mode = app.game.ui.display_mode;
-            app.selected_color = None;
-            if app.game.bot.is_some() {
-                app.game.bot = None;
+            KeyCode::Char('b') => {
+                let display_mode = app.game.ui.display_mode;
+                app.selected_color = None;
+                if app.game.bot.is_some() {
+                    app.game.bot = None;
+                }
+                if app.game.opponent.is_some() {
+                    app.game
+                        .opponent
+                        .as_mut()
+                        .unwrap()
+                        .send_end_game_to_server();
+                    app.game.opponent = None;
+                    app.hosting = None;
+                    app.host_ip = None;
+                }
+
+                app.go_to_home();
+                app.game.game_board.reset();
+                app.game.ui.reset();
+                app.game.ui.display_mode = display_mode;
             }
-            app.go_to_home();
-            app.game.game_board.reset();
-            app.game.ui.reset();
-            app.game.ui.display_mode = display_mode;
+            // Other handlers you could add here.
+            _ => {}
         }
-        // Other handlers you could add here.
-        _ => {}
     }
+
     Ok(())
 }
 
 pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
     // Mouse control only implemented for actual game
-    if app.current_page == Pages::Home {
+    if app.current_page == Pages::Home || app.current_page == Pages::Credit {
         return Ok(());
     }
     if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
@@ -156,7 +257,7 @@ pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<
             return Ok(());
         }
 
-        if app.show_color_popup || app.show_help_popup {
+        if app.current_popup.is_some() {
             return Ok(());
         }
 
@@ -170,6 +271,9 @@ pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<
             }
             app.game.ui.promotion_cursor = x as i8;
             app.game.promote_piece();
+            if app.game.opponent.is_some() {
+                app.game.handle_multiplayer_promotion();
+            }
         }
         if mouse_event.column < app.game.ui.top_x || mouse_event.row < app.game.ui.top_y {
             return Ok(());
@@ -199,7 +303,7 @@ pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<
             }
         {
             app.game.ui.cursor_coordinates = coords;
-            app.game.select_cell();
+            app.game.handle_cell_click();
         } else {
             app.game.ui.selected_coordinates = coords;
         }
diff --git a/src/lib.rs b/src/lib.rs
index e8751aa..a6ab415 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,6 +7,8 @@ pub mod event;
 /// Widget renderer.
 pub mod ui;
 
+pub mod server;
+
 /// Event handler.
 pub mod handler;
 
diff --git a/src/main.rs b/src/main.rs
index 6198095..2d19686 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,11 +5,13 @@ use chess_tui::app::{App, AppResult};
 use chess_tui::constants::{home_dir, DisplayMode};
 use chess_tui::event::{Event, EventHandler};
 use chess_tui::game_logic::game::GameState;
+use chess_tui::game_logic::opponent::wait_for_game_start;
 use chess_tui::handler::{handle_key_events, handle_mouse_events};
 use chess_tui::ui::tui::Tui;
 use clap::Parser;
 use std::fs::{self, File};
 use std::io::Write;
+use std::panic;
 use std::path::Path;
 use toml::Value;
 
@@ -39,7 +41,7 @@ fn main() -> AppResult<()> {
     config_create(&args, &folder_path, &config_path)?;
 
     // Create an application.
-    let mut app: App = App::default();
+    let mut app = App::default();
 
     // We store the chess engine path if there is one
     if let Ok(content) = fs::read_to_string(config_path) {
@@ -67,6 +69,17 @@ fn main() -> AppResult<()> {
     let events = EventHandler::new(250);
     let mut tui = Tui::new(terminal, events);
 
+    let default_panic = std::panic::take_hook();
+    panic::set_hook(Box::new(move |info| {
+        ratatui::restore();
+        ratatui::crossterm::execute!(
+            std::io::stdout(),
+            ratatui::crossterm::event::DisableMouseCapture
+        )
+        .unwrap();
+        default_panic(info);
+    }));
+
     // Start the main loop.
     while app.running {
         // Render the user interface.
@@ -92,6 +105,45 @@ fn main() -> AppResult<()> {
             }
             tui.draw(&mut app)?;
         }
+
+        if app.game.opponent.is_some()
+            && app
+                .game
+                .opponent
+                .as_ref()
+                .map_or(false, |opponent| !opponent.game_started)
+        {
+            let opponent = app.game.opponent.as_mut().unwrap();
+            wait_for_game_start(opponent.stream.as_ref().unwrap());
+            opponent.game_started = true;
+            app.current_popup = None;
+        }
+
+        // If it's the opponent turn, wait for the opponent to move
+        if app.game.opponent.is_some()
+            && app
+                .game
+                .opponent
+                .as_ref()
+                .map_or(false, |opponent| opponent.opponent_will_move)
+        {
+            tui.draw(&mut app)?;
+
+            if !app.game.game_board.is_checkmate(app.game.player_turn)
+                && !app.game.game_board.is_draw(app.game.player_turn)
+            {
+                app.game.execute_opponent_move();
+                app.game.switch_player_turn();
+            }
+
+            // need to be centralised
+            if app.game.game_board.is_checkmate(app.game.player_turn) {
+                app.game.game_state = GameState::Checkmate;
+            } else if app.game.game_board.is_draw(app.game.player_turn) {
+                app.game.game_state = GameState::Draw;
+            }
+            tui.draw(&mut app)?;
+        }
     }
 
     // Exit the user interface.
diff --git a/src/server/game_server.rs b/src/server/game_server.rs
new file mode 100644
index 0000000..01cc0d0
--- /dev/null
+++ b/src/server/game_server.rs
@@ -0,0 +1,148 @@
+use std::{
+    io::{Read, Write},
+    net::{TcpListener, TcpStream},
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        mpsc, Arc, Mutex,
+    },
+    thread,
+};
+
+#[derive(Debug)]
+pub struct Client {
+    addr: String,
+    stream: TcpStream,
+}
+
+#[derive(Clone)]
+pub struct GameServer {
+    pub clients: Arc<Mutex<Vec<Client>>>,
+    pub client_id: usize,
+    pub is_host_white: bool,
+    pub stop_signal: Arc<AtomicBool>,
+}
+
+impl GameServer {
+    pub fn new(is_host_white: bool) -> Self {
+        Self {
+            clients: Arc::new(Mutex::new(vec![])),
+            client_id: 0,
+            is_host_white,
+            stop_signal: Arc::new(AtomicBool::new(false)),
+        }
+    }
+
+    pub fn run(&self) {
+        let listener = TcpListener::bind("0.0.0.0:2308").expect("Failed to create listener");
+        listener
+            .set_nonblocking(true)
+            .expect("Failed to set listener to non-blocking");
+
+        let state = self.clients.clone();
+        let stop_signal = self.stop_signal.clone();
+        let (shutdown_tx, shutdown_rx) = mpsc::channel();
+
+        // Spawn a thread to watch for the stop signal
+        let stop_signal_clone = stop_signal.clone();
+        thread::spawn(move || {
+            while !stop_signal_clone.load(Ordering::SeqCst) {
+                thread::sleep(std::time::Duration::from_millis(100));
+            }
+            let _ = shutdown_tx.send(());
+        });
+
+        loop {
+            // Check for shutdown signal
+            if shutdown_rx.try_recv().is_ok() {
+                break;
+            }
+
+            // Handle incoming connections
+            match listener.accept() {
+                Ok((mut stream, _addr)) => {
+                    let state = Arc::clone(&state);
+                    let stop_signal = Arc::clone(&stop_signal);
+                    let color = if self.is_host_white { "w" } else { "b" };
+
+                    thread::spawn(move || {
+                        {
+                            let mut state_lock = state.lock().unwrap();
+                            // There is already one player (host who choose the color) we will need to send the color to the joining player and inform the host of the game start
+                            if state_lock.len() == 1 {
+                                stream.write_all(color.as_bytes()).unwrap();
+                                let other_player = state_lock.last().unwrap();
+                                let mut other_player_stream =
+                                    other_player.stream.try_clone().unwrap();
+                                other_player_stream.write_all("s".as_bytes()).unwrap();
+                            } else if state_lock.len() >= 2 {
+                                stream.write_all("Game is already full".as_bytes()).unwrap();
+                                return;
+                            }
+
+                            state_lock.push(Client {
+                                addr: stream.peer_addr().unwrap().to_string(),
+                                stream: stream.try_clone().unwrap(),
+                            });
+                        }
+                        handle_client(state, stop_signal, stream);
+                    });
+                }
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
+                    // No connection ready, sleep briefly
+                    thread::sleep(std::time::Duration::from_millis(100));
+                }
+                Err(e) => {
+                    eprintln!("Failed to accept connection: {}", e);
+                }
+            }
+        }
+    }
+}
+
+fn handle_client(
+    state: Arc<Mutex<Vec<Client>>>,
+    stop_signal: Arc<AtomicBool>,
+    mut stream: TcpStream,
+) {
+    loop {
+        let mut buffer = [0; 5];
+        let addr = stream.peer_addr().unwrap().to_string();
+        let bytes_read = stream.read(&mut buffer).unwrap_or(0);
+
+        if bytes_read == 0 {
+            broadcast_message(state.clone(), "ended".to_string(), &addr);
+            remove_client(&state, &addr);
+            // we stop the server if one of the clients disconnects
+            stop_signal.store(true, Ordering::SeqCst);
+            break;
+        }
+
+        let request = String::from_utf8_lossy(&buffer[..]);
+        broadcast_message(state.clone(), format!("{}", request), &addr);
+
+        if request.trim() == "ended" {
+            remove_client(&state, &addr);
+            // We stop the server if one of the clients disconnects
+            stop_signal.store(true, Ordering::SeqCst);
+            break;
+        }
+    }
+}
+
+fn broadcast_message(state: Arc<Mutex<Vec<Client>>>, message: String, sender_addr: &String) {
+    let state = state.lock().unwrap();
+    for client in state.iter() {
+        if &client.addr == sender_addr {
+            continue;
+        }
+        let mut client_stream = client.stream.try_clone().unwrap();
+        client_stream.write_all(message.as_bytes()).unwrap();
+    }
+}
+
+fn remove_client(state: &Arc<Mutex<Vec<Client>>>, addr: &str) {
+    let mut state_lock = state.lock().unwrap();
+    if let Some(index) = state_lock.iter().position(|client| client.addr == addr) {
+        state_lock.remove(index);
+    }
+}
diff --git a/src/server/mod.rs b/src/server/mod.rs
new file mode 100644
index 0000000..90c0122
--- /dev/null
+++ b/src/server/mod.rs
@@ -0,0 +1,1 @@
+pub mod game_server;
diff --git a/src/ui/main_ui.rs b/src/ui/main_ui.rs
index 1da5e35..374f698 100644
--- a/src/ui/main_ui.rs
+++ b/src/ui/main_ui.rs
@@ -8,6 +8,7 @@ use ratatui::{
 };
 
 use crate::{
+    constants::Popups,
     game_logic::{bot::Bot, game::GameState},
     ui::popups::{
         render_color_selection_popup, render_credit_popup, render_end_popup,
@@ -15,6 +16,9 @@ use crate::{
     },
 };
 
+use super::popups::{
+    render_enter_multiplayer_ip, render_multiplayer_selection_popup, render_wait_for_other_player,
+};
 use crate::{
     app::App,
     constants::{DisplayMode, Pages, TITLE},
@@ -22,16 +26,40 @@ use crate::{
 };
 
 /// Renders the user interface widgets.
-pub fn render(app: &mut App, frame: &mut Frame) {
+pub fn render(app: &mut App, frame: &mut Frame<'_>) {
     let main_area = frame.area();
 
+    // Solo game
     if app.current_page == Pages::Solo {
         render_game_ui(frame, app, main_area);
-    } else if app.current_page == Pages::Bot {
+    }
+    // Multiplayer game
+    else if app.current_page == Pages::Multiplayer {
+        if app.hosting.is_none() {
+            app.current_popup = Some(Popups::MultiplayerSelection);
+        } else if app.selected_color.is_none() && app.hosting.unwrap() {
+            app.current_popup = Some(Popups::ColorSelection);
+        } else if app.game.opponent.is_none() {
+            if app.host_ip.is_none() {
+                if app.hosting.is_some() && app.hosting.unwrap() {
+                    app.setup_game_server(app.selected_color.unwrap());
+                    app.host_ip = Some("127.0.0.1".to_string());
+                } else {
+                    app.current_popup = Some(Popups::EnterHostIP);
+                }
+            } else {
+                app.create_opponent();
+            }
+        } else if app.game.opponent.as_mut().unwrap().game_started {
+            render_game_ui(frame, app, main_area);
+        }
+    }
+    // Play against bot
+    else if app.current_page == Pages::Bot {
         if app.chess_engine_path.is_none() || app.chess_engine_path.as_ref().unwrap().is_empty() {
             render_engine_path_error_popup(frame);
         } else if app.selected_color.is_none() {
-            app.show_color_popup = true;
+            app.current_popup = Some(Popups::ColorSelection);
         } else if app.game.bot.is_none() {
             let engine_path = app.chess_engine_path.clone().unwrap();
             let is_bot_starting = app.selected_color.unwrap() == PieceColor::Black;
@@ -39,20 +67,35 @@ pub fn render(app: &mut App, frame: &mut Frame) {
         } else {
             render_game_ui(frame, app, main_area);
         }
-    } else {
-        render_menu_ui(frame, app, main_area);
     }
-    if app.show_color_popup {
-        render_color_selection_popup(frame, app);
-    }
-
-    if app.show_help_popup {
-        render_help_popup(frame);
+    // Render menu
+    else {
+        render_menu_ui(frame, app, main_area);
     }
 
     if app.current_page == Pages::Credit {
         render_credit_popup(frame);
     }
+
+    // Render popups
+    match app.current_popup {
+        Some(Popups::ColorSelection) => {
+            render_color_selection_popup(frame, app);
+        }
+        Some(Popups::MultiplayerSelection) => {
+            render_multiplayer_selection_popup(frame, app);
+        }
+        Some(Popups::EnterHostIP) => {
+            render_enter_multiplayer_ip(frame, &app.game.ui.prompt);
+        }
+        Some(Popups::WaitingForOpponentToJoin) => {
+            render_wait_for_other_player(frame, app.get_host_ip());
+        }
+        Some(Popups::Help) => {
+            render_help_popup(frame);
+        }
+        _ => {}
+    }
 }
 
 /// Helper function to create a centered rect using up certain percentage of the available rect `r`
@@ -123,6 +166,7 @@ pub fn render_menu_ui(frame: &mut Frame, app: &App, main_area: Rect) {
     // Board block representing the full board div
     let menu_items = [
         "Normal game",
+        "Multiplayer",
         "Play against a bot",
         &display_mode_menu,
         "Help",
@@ -149,7 +193,7 @@ pub fn render_menu_ui(frame: &mut Frame, app: &App, main_area: Rect) {
 }
 
 // Method to render the game board and handle game popups
-pub fn render_game_ui(frame: &mut Frame, app: &mut App, main_area: Rect) {
+pub fn render_game_ui(frame: &mut Frame<'_>, app: &mut App, main_area: Rect) {
     let main_layout_horizontal = Layout::default()
         .direction(Direction::Vertical)
         .constraints(
@@ -232,10 +276,14 @@ pub fn render_game_ui(frame: &mut Frame, app: &mut App, main_area: Rect) {
             PieceColor::Black => "Black",
         };
 
-        render_end_popup(frame, &format!("{string_color} Won !!!"));
+        render_end_popup(
+            frame,
+            &format!("{string_color} Won !!!"),
+            app.game.opponent.is_some(),
+        );
     }
 
     if app.game.game_state == GameState::Draw {
-        render_end_popup(frame, "That's a draw");
+        render_end_popup(frame, "That's a draw", app.game.opponent.is_some());
     }
 }
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 8d74b47..d46c76f 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,3 +1,4 @@
 pub mod main_ui;
 pub mod popups;
+pub mod prompt;
 pub mod tui;
diff --git a/src/ui/popups.rs b/src/ui/popups.rs
index 4317c43..6af956c 100644
--- a/src/ui/popups.rs
+++ b/src/ui/popups.rs
@@ -1,3 +1,5 @@
+use std::net::IpAddr;
+
 use crate::{
     app::App,
     constants::WHITE,
@@ -5,13 +7,15 @@ use crate::{
     ui::main_ui::centered_rect,
 };
 use ratatui::{
-    layout::{Alignment, Constraint, Direction, Layout},
-    style::{Color, Style, Stylize},
-    text::Line,
+    layout::{Alignment, Constraint, Direction, Layout, Position},
+    style::{Color, Modifier, Style, Stylize},
+    text::{Line, Span, Text},
     widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, Wrap},
     Frame,
 };
 
+use super::prompt::Prompt;
+
 // This renders a popup when the selected game mode is bot and there is no chess engine path
 pub fn render_engine_path_error_popup(frame: &mut Frame) {
     let block = Block::default()
@@ -42,7 +46,7 @@ pub fn render_engine_path_error_popup(frame: &mut Frame) {
 }
 
 // This renders a popup for a promotion
-pub fn render_end_popup(frame: &mut Frame, sentence: &str) {
+pub fn render_end_popup(frame: &mut Frame, sentence: &str, is_multiplayer: bool) {
     let block = Block::default()
         .title("Game ended")
         .borders(Borders::ALL)
@@ -55,7 +59,12 @@ pub fn render_end_popup(frame: &mut Frame, sentence: &str) {
         Line::from(sentence).alignment(Alignment::Center),
         Line::from(""),
         Line::from(""),
-        Line::from("Press `R` to start a new game").alignment(Alignment::Center),
+        Line::from(if is_multiplayer {
+            "Press `B` to go back to the menu"
+        } else {
+            "Press `R` to restart a new game"
+        })
+        .alignment(Alignment::Center),
     ];
 
     let paragraph = Paragraph::new(text)
@@ -68,6 +77,7 @@ pub fn render_end_popup(frame: &mut Frame, sentence: &str) {
     frame.render_widget(paragraph, area);
 }
 
+// This renders a popup for a promotion
 pub fn render_promotion_popup(frame: &mut Frame, app: &mut App) {
     let block = Block::default()
         .title("Pawn promotion")
@@ -214,7 +224,7 @@ pub fn render_help_popup(frame: &mut Frame) {
         Line::from("Game controls:".underlined().bold()),
         Line::from(""),
         Line::from(vec![
-            "←/h ↑/k ↓/j →/l: Use these keys to move the ".into(),
+            "←/h ↑/k ↓/j →/l: Use these keys or the mouse to move the ".into(),
             "blue".blue(),
             " cursor".into(),
         ]),
@@ -336,3 +346,149 @@ pub fn render_color_selection_popup(frame: &mut Frame, app: &App) {
         );
     frame.render_widget(black_pawn, inner_popup_layout_horizontal[2]);
 }
+
+// This renders a popup for the multiplayer hosting / joining popup
+pub fn render_multiplayer_selection_popup(frame: &mut Frame, app: &App) {
+    let block: Block<'_> = Block::default()
+        .borders(Borders::ALL)
+        .border_type(BorderType::Rounded)
+        .padding(Padding::horizontal(1))
+        .border_style(Style::default().fg(WHITE));
+    let area = centered_rect(40, 40, frame.area());
+
+    let text = vec![
+        Line::from(""),
+        Line::from("-- Are you hosting or joining a game ? --").alignment(Alignment::Center),
+        Line::from(""),
+    ];
+
+    let paragraph = Paragraph::new(text)
+        .block(Block::default())
+        .alignment(Alignment::Left)
+        .wrap(Wrap { trim: true });
+    frame.render_widget(Clear, area);
+    frame.render_widget(block, area);
+    frame.render_widget(paragraph, area);
+
+    let inner_popup_layout_vertical = Layout::default()
+        .direction(Direction::Vertical)
+        .constraints(
+            [
+                Constraint::Ratio(1, 3),
+                Constraint::Ratio(1, 3),
+                Constraint::Ratio(1, 3),
+            ]
+            .as_ref(),
+        )
+        .split(area);
+
+    let inner_popup_layout_horizontal = Layout::default()
+        .direction(Direction::Horizontal)
+        .constraints(
+            [
+                Constraint::Ratio(1, 3),
+                Constraint::Ratio(1, 3),
+                Constraint::Ratio(1, 3),
+            ]
+            .as_ref(),
+        )
+        .split(inner_popup_layout_vertical[1]);
+
+    let hosting = Paragraph::new(Text::from(vec![Line::from(vec![Span::styled(
+        "HOSTING",
+        Style::default().add_modifier(if app.menu_cursor == 0 {
+            Modifier::UNDERLINED
+        } else {
+            Modifier::empty()
+        }),
+    )])]))
+    .block(Block::default())
+    .alignment(Alignment::Center);
+
+    frame.render_widget(hosting, inner_popup_layout_horizontal[0]);
+
+    let joining = Paragraph::new(Text::from(vec![Line::from(vec![Span::styled(
+        "JOINING",
+        Style::default().add_modifier(if app.menu_cursor == 1 {
+            Modifier::UNDERLINED
+        } else {
+            Modifier::empty()
+        }),
+    )])]))
+    .block(Block::default())
+    .alignment(Alignment::Center);
+    frame.render_widget(joining, inner_popup_layout_horizontal[2]);
+}
+
+// MULTIPLAYER POPUPS
+// This renders a popup indicating we are waiting for the other player
+pub fn render_wait_for_other_player(frame: &mut Frame, ip: IpAddr) {
+    let block = Block::default()
+        .title("Waiting ...")
+        .borders(Borders::ALL)
+        .border_type(BorderType::Rounded)
+        .padding(Padding::horizontal(1))
+        .border_style(Style::default().fg(WHITE));
+    let area = centered_rect(40, 40, frame.area());
+
+    let text = vec![
+        Line::from(""),
+        Line::from(""),
+        Line::from("Waiting for other player").alignment(Alignment::Center),
+        Line::from(format!("Host IP address and port: {}:2308", ip)).alignment(Alignment::Center),
+    ];
+
+    let paragraph = Paragraph::new(text)
+        .block(block.clone())
+        .alignment(Alignment::Left)
+        .wrap(Wrap { trim: true });
+
+    frame.render_widget(Clear, area); //this clears out the background
+    frame.render_widget(block, area);
+    frame.render_widget(paragraph, area);
+}
+
+// This renders a popup allowing us to get a user input
+pub fn render_enter_multiplayer_ip(frame: &mut Frame, prompt: &Prompt) {
+    let block = Block::default()
+        .title("Join a game")
+        .borders(Borders::ALL)
+        .border_type(BorderType::Rounded)
+        .padding(Padding::horizontal(1))
+        .border_style(Style::default().fg(WHITE));
+    let area = centered_rect(40, 40, frame.area());
+
+    let current_input = prompt.input.as_str();
+
+    let text = vec![
+        Line::from("Enter the ip address and port of the host:").alignment(Alignment::Center),
+        Line::from(""),
+        Line::from(current_input),
+        Line::from(""),
+        Line::from(""),
+        Line::from(""),
+        Line::from(""),
+        Line::from("Example: 10.111.6.50:2308;"),
+        Line::from("Documentation: https://thomas-mauran.github.io/chess-tui/docs/Multiplayer/Online%20multiplayer/"),
+        Line::from(""),
+        Line::from(""),
+        Line::from("Press `Esc` to close the popup.").alignment(Alignment::Center),
+    ];
+
+    let paragraph = Paragraph::new(text)
+        .block(block.clone())
+        .alignment(Alignment::Left)
+        .wrap(Wrap { trim: true });
+
+    frame.set_cursor_position(Position::new(
+        // Draw the cursor at the current position in the input field.
+        // This position is can be controlled via the left and right arrow key
+        area.x + prompt.character_index as u16 + 2,
+        // Move one line down, from the border to the input line
+        area.y + 3,
+    ));
+
+    frame.render_widget(Clear, area); //this clears out the background
+    frame.render_widget(block, area);
+    frame.render_widget(paragraph, area);
+}
diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs
new file mode 100644
index 0000000..7eef693
--- /dev/null
+++ b/src/ui/prompt.rs
@@ -0,0 +1,87 @@
+/// App holds the state of the application
+
+#[derive(Clone, Default)]
+pub struct Prompt {
+    /// Current value of the input box
+    pub input: String,
+    /// Position of cursor in the editor area.
+    pub character_index: usize,
+    /// The prompt entry message
+    pub message: String,
+}
+
+impl Prompt {
+    pub fn new() -> Self {
+        Self {
+            input: "".to_string(),
+            character_index: 0,
+            message: String::new(),
+        }
+    }
+
+    pub fn move_cursor_left(&mut self) {
+        let cursor_moved_left = self.character_index.saturating_sub(1);
+        self.character_index = self.clamp_cursor(cursor_moved_left);
+    }
+
+    pub fn move_cursor_right(&mut self) {
+        let cursor_moved_right = self.character_index.saturating_add(1);
+        self.character_index = self.clamp_cursor(cursor_moved_right);
+    }
+
+    pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
+        new_cursor_pos.clamp(0, self.input.chars().count())
+    }
+
+    pub fn reset_cursor(&mut self) {
+        self.character_index = 0;
+    }
+
+    pub fn submit_message(&mut self) {
+        self.message = self.input.clone();
+        self.input.clear();
+        self.reset_cursor();
+    }
+
+    pub fn enter_char(&mut self, new_char: char) {
+        let index = self.byte_index();
+        if index < 40 {
+            self.input.insert(index, new_char);
+            self.move_cursor_right();
+        }
+    }
+
+    /// Returns the byte index based on the character position.
+    ///
+    /// Since each character in a string can be contain multiple bytes, it's necessary to calculate
+    /// the byte index based on the index of the character.
+    pub fn byte_index(&self) -> usize {
+        self.input
+            .char_indices()
+            .map(|(i, _)| i)
+            .nth(self.character_index)
+            .unwrap_or(self.input.len())
+    }
+
+    pub fn delete_char(&mut self) {
+        let is_not_cursor_leftmost = self.character_index != 0;
+        if is_not_cursor_leftmost {
+            // Method "remove" is not used on the saved text for deleting the selected char.
+            // Reason: Using remove on String works on bytes instead of the chars.
+            // Using remove would require special care because of char boundaries.
+
+            let current_index = self.character_index;
+            let from_left_to_current_index = current_index - 1;
+
+            // Getting all characters before the selected character.
+            let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
+            // Getting all characters after selected character.
+            let after_char_to_delete = self.input.chars().skip(current_index);
+
+            // Put all characters together except the selected one.
+            // By leaving the selected one out, it is forgotten and therefore deleted.
+            self.input = before_char_to_delete.chain(after_char_to_delete).collect();
+            self.move_cursor_left();
+        }
+    }
+}
diff --git a/src/ui/tui.rs b/src/ui/tui.rs
index 548c887..9c7c5c8 100644
--- a/src/ui/tui.rs
+++ b/src/ui/tui.rs
@@ -26,7 +26,9 @@ impl<B: Backend> Tui<B> {
     ///
     /// [`Draw`]: ratatui::Terminal::draw
     /// [`rendering`]: crate::ui:render
+    // Créer une fonction async pour le rendu
     pub fn draw(&mut self, app: &mut App) -> AppResult<()> {
+        // Passe une closure synchrone qui appelle la fonction async
         self.terminal.draw(|frame| main_ui::render(app, frame))?;
         Ok(())
     }
diff --git a/website/docs/Code Architecture/Game.md b/website/docs/Code Architecture/Game.md
index e6cb0c5..76102f1 100644
--- a/website/docs/Code Architecture/Game.md
+++ b/website/docs/Code Architecture/Game.md
@@ -3,36 +3,44 @@
 ```mermaid
 classDiagram
 
-    class App {
+class App {
         +bool running
         +Game game
         +Pages current_page
-        +bool show_help_popup
-        +bool show_color_popup
-        +Option~PieceColor~ selected_color
+        +Option<Popups> current_popup
+        +Option<PieceColor> selected_color
+        +Option<bool> hosting
+        +Option<String> host_ip
         +u8 menu_cursor
-        +Option~String~ chess_engine_path
+        +Option<String> chess_engine_path
+
         +toggle_help_popup()
         +toggle_credit_popup()
         +go_to_home()
         +tick()
         +quit()
-        +menu_cursor_up(u8)
-        +menu_cursor_right(u8)
-        +menu_cursor_left(u8)
-        +menu_cursor_down(u8)
+        +menu_cursor_up(l: u8)
+        +menu_cursor_right(l: u8)
+        +menu_cursor_left(l: u8)
+        +menu_cursor_down(l: u8)
         +color_selection()
         +restart()
         +menu_select()
         +update_config()
+        +setup_game_server(host_color: PieceColor)
+        +create_opponent()
+        +hosting_selection()
+        +bot_setup()
+        +get_host_ip() IpAddr
     }
-
     class Game {
         +GameBoard game_board
         +UI ui
         +Option<Bot> bot
+        +Option<Opponent> opponent
         +PieceColor player_turn
         +GameState game_state
+
         +new(game_board: GameBoard, player_turn: PieceColor)
         +set_board(game_board: GameBoard)
         +set_player_turn(player_turn: PieceColor)
@@ -41,6 +49,11 @@ classDiagram
         +execute_bot_move()
         +promote_piece()
         +execute_move(from: Coord, to: Coord)
+        +handle_cell_click()
+        +already_selected_cell_action()
+        +handle_promotion()
+        +execute_opponent_move()
+        +handle_multiplayer_promotion()
     }
 
     class GameBoard {
@@ -96,7 +109,20 @@ classDiagram
         +set_engine(engine_path: &str)
         +create_engine(engine_path: &str): Engine
         +get_bot_move(fen_position: String): String
+    }
 
+    class Opponent {
+        +Option<TcpStream> stream
+        +bool opponent_will_move
+        +PieceColor color
+        +bool game_started
+
+        +copy() : Opponent
+        +new(addr: String, color: Option<PieceColor>) : Opponent
+        +start_stream(addr: &str) : void
+        +send_end_game_to_server() : void
+        +send_move_to_server(move_to_send: &PieceMove, promotion_type: Option<String>) : void
+        +read_stream() : String
     }
 
     class Coord {
@@ -149,6 +175,8 @@ classDiagram
     PieceMove "1" --> "1" Coord : from_to
     Coord "1" --> "1" PieceColor : color
     Coord "1" --> "1" PieceType : type
+    Game "1" --> "0..1" Opponent : "has"
+    Opponent "1" --> "1" PieceColor : color
 ```
 
 
diff --git a/website/docs/Multiplayer/Local multiplayer.md b/website/docs/Multiplayer/Local multiplayer.md
new file mode 100644
index 0000000..6cb2abb
--- /dev/null
+++ b/website/docs/Multiplayer/Local multiplayer.md
@@ -0,0 +1,8 @@
+# Local Multiplayer
+
+The local multiplayer feature is available in the `Normal game` menu option. You can play chess with your friends on the same computer using this feature.
+
+
+Each turn the board will turn allowing your opponent to play. The game will continue until one of the players wins or the game ends in a draw.
+
+![Demo](../../static/gif/demo-two-player.gif)
\ No newline at end of file
diff --git a/website/docs/Multiplayer/Online multiplayer.md b/website/docs/Multiplayer/Online multiplayer.md
new file mode 100644
index 0000000..6acd2a3
--- /dev/null
+++ b/website/docs/Multiplayer/Online multiplayer.md
@@ -0,0 +1,74 @@
+# Online Multiplayer
+
+You can now play chess with your friends online. The online multiplayer feature is available in the `Multiplayer` menu option.
+
+![multiplayer gif demo](../../static/gif/multiplayer.gif)
+
+
+## LAN
+
+If you are on the same network as your friend you don't have anything to worry about. One of the player need to choose `Host` and the other player need to choose `Join`. The player who is hosting the game will get it's ip displayed on the screen. The other player need to enter the `ip`:2308 and click on `Join`.
+
+By default the game will be hosted on port 2308, make sure you had :2308 at the end of the ip address.
+
+## WLAN
+
+If you are not on the same network as your friend you need to do some port forwarding, but don't worry tools allows you to do that in one command !
+
+For this we will use [Bore](https://github.com/ekzhang/bore) which is an open source rust written tool that allows you to expose your local server to the internet.
+
+First you need to install bore, you can do that by running the following command:
+
+```bash
+cargo install bore
+```
+
+Then you need to create a tcp tunnel to your local server, you can do that by running the following command:
+
+```bash
+bore local 2308 --to bore.pub
+```
+
+this will create a tunnel to your local server on port 2308 to bore.pub, once done you will see the following message:
+![Bore port](bore-port.png)
+
+This means that you can access the game on bore.pub:12455 (the port will obviously be different).
+
+The other player then only need to enter bore.pub:port_given to join the game.
+
+Here for example it would be `bore.pub:12455`
+
+### How does it work ?
+
+When you host a game a new thread will be created running a game_server instance that will listen on the port 2308. This Game Server will handle 2 clients at max and will simply forward the messages between the 2 clients. In the meantime the main thread creates a new Player instance which represent a connection to the game server.
+
+If you are joining a game you are not creating a game server but simply creating a Player instance that will connect to the game server address.
+
+```mermaid
+graph TD
+    A[Start] -->|Host Game| B[Main Thread Creates Game Server]
+    B --> C[Game Server Listens on Port 2308]
+    B --> F[Main Thread Creates Player Instance]
+    F --> G[Player Instance Connects to Game Server]
+    A -->|Join Game| H[Create Player Instance]
+    H --> I[Player Connects to Game Server Address]
+    G --> C
+    I --> C
+```
+
+### Message exchange
+
+The message exchange between the clients and the server is done using a simple protocol with the following terms:
+
+- `b` : Player will play with black pieces
+- `w` : Player will play with white pieces
+- `s` : The game has started
+- `ended` : The game has ended
+- `e4e5` : A move from e4 to e5
+- `e6e7q` : A move from e6 to e7 with a promotion to queen
+
+When we are hosting we choose a color and then wait for the `s` message to be sent to start the game. When we are joining we wait for the color `b` or `w` message then for the `s` message to start the game.
+
+When the game is started the server will send the `s` message to both clients and the game will start. The clients will then send the moves to the server and the server will forward the moves to the other client.
+
+When the game ends the server will send the `ended` message to both clients and the game will be over.
diff --git a/website/docs/Multiplayer/bore-port.png b/website/docs/Multiplayer/bore-port.png
new file mode 100644
index 0000000..a0ce917
Binary files /dev/null and b/website/docs/Multiplayer/bore-port.png differ
diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts
index 3152298..1277ff4 100644
--- a/website/docusaurus.config.ts
+++ b/website/docusaurus.config.ts
@@ -1,5 +1,5 @@
-import {themes as prismThemes} from 'prism-react-renderer';
-import type {Config} from '@docusaurus/types';
+import { themes as prismThemes } from 'prism-react-renderer';
+import type { Config } from '@docusaurus/types';
 import type * as Preset from '@docusaurus/preset-classic';
 
 // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
@@ -11,23 +11,16 @@ const config: Config = {
 
   // Set the production url of your site here
   url: 'https://thomas-mauran.github.io',
-  // Set the /<baseUrl>/ pathname under which your site is served
-  // For GitHub pages deployment, it is often '/<projectName>/'
   baseUrl: '/chess-tui/',
 
-  // GitHub pages deployment config.
-  // If you aren't using GitHub pages, you don't need these.
-  organizationName: 'thomas-mauran', // Usually your GitHub org/user name.
-  projectName: 'chess-tui', // Usually your repo name.
+  organizationName: 'thomas-mauran',
+  projectName: 'chess-tui',
   deploymentBranch: 'gh-pages',
   trailingSlash: false,
 
   onBrokenLinks: 'throw',
   onBrokenMarkdownLinks: 'warn',
 
-  // Even if you don't use internationalization, you can use this field to set
-  // useful metadata like html lang. For example, if your site is Chinese, you
-  // may want to replace "en" with "zh-Hans".
   i18n: {
     defaultLocale: 'en',
     locales: ['en'],
@@ -39,8 +32,6 @@ const config: Config = {
       {
         docs: {
           sidebarPath: './sidebars.ts',
-          // Please change this to your repo.
-          // Remove this to remove the "edit this page" links.
           editUrl:
             'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
         },
@@ -50,11 +41,8 @@ const config: Config = {
             type: ['rss', 'atom'],
             xslt: true,
           },
-          // Please change this to your repo.
-          // Remove this to remove the "edit this page" links.
           editUrl:
             'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
-          // Useful options to enforce blogging best practices
           onInlineTags: 'warn',
           onInlineAuthors: 'warn',
           onUntruncatedBlogPosts: 'warn',
@@ -67,7 +55,6 @@ const config: Config = {
   ],
 
   themeConfig: {
-    // Replace with your project's social card
     image: 'img/social-card.png',
     navbar: {
       title: 'Chess-tui',
@@ -82,11 +69,13 @@ const config: Config = {
           position: 'left',
           label: 'Documentation',
         },
-        {to: '/blog', label: 'Blog', position: 'left'},
+        { to: '/blog', label: 'Blog', position: 'left' },
         {
           href: 'https://github.com/thomas-mauran/chess-tui',
-          label: 'GitHub',
           position: 'right',
+          className: 'header-github-link',
+          'aria-label': 'GitHub repository',
+          html: `<img alt="GitHub Stars" src="https://img.shields.io/github/stars/thomas-mauran/chess-tui.svg?style=social&label=Star" />`,
         },
       ],
     },
@@ -127,9 +116,6 @@ const config: Config = {
       ],
       copyright: `Copyright © ${new Date().getFullYear()} Thomas Mauran, Inc. Built with Docusaurus.`,
     },
-
-
-  
   } satisfies Preset.ThemeConfig,
 
   markdown: {
diff --git a/website/sidebars.ts b/website/sidebars.ts
index cf1a13b..72bb661 100644
--- a/website/sidebars.ts
+++ b/website/sidebars.ts
@@ -8,7 +8,11 @@ const sidebars: SidebarsConfig = {
       label: 'Installation', 
       items: ['Installation/Packaging status', 'Installation/Cargo', 'Installation/Build from source', 'Installation/NetBSD', 'Installation/Arch Linux', 'Installation/NixOS', 'Installation/Docker'], 
     },
-
+    {
+      type: 'category',
+      label: 'Multiplayer', 
+      items: ['Multiplayer/Local multiplayer', 'Multiplayer/Online multiplayer'], 
+    },
     {
       type: 'category',
       label: 'Code Architecture', 
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index a7d2f8a..dbe423c 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -53,3 +53,24 @@ footer.footer a {
 footer.footer a:hover {
   color: var(--ifm-color-primary-lighter); /* Slightly warmer color for hover state */
 }
+
+/* Ensure the theme toggle button aligns properly */
+.navbar__toggle {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+/* Adjust spacing or alignment if necessary */
+.navbar__toggle svg {
+  vertical-align: middle;
+  height: 1.2em; /* Adjust size to match GitHub icons */
+  margin-left: 8px; /* Fine-tune the spacing */
+  margin-right: 8px; /* Fine-tune the spacing */
+}
+
+/* Adjust GitHub badge alignment */
+.header-github-link img {
+  vertical-align: middle;
+}
diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx
index e8e8db5..677f8f5 100644
--- a/website/src/pages/index.tsx
+++ b/website/src/pages/index.tsx
@@ -26,7 +26,7 @@ function HomepageHeader() {
           <Link
             className="button button--secondary button--lg"
             to="/docs/intro">
-            Let's start ♟️
+            Play now ♟️
           </Link>
         </div>
       </div>
diff --git a/website/src/pages/markdown-page.md b/website/src/pages/markdown-page.md
deleted file mode 100644
index 9756c5b..0000000
--- a/website/src/pages/markdown-page.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Markdown page example
----
-
-# Markdown page example
-
-You don't need React to write simple standalone pages.
diff --git a/website/static/gif/demo-two-player.gif b/website/static/gif/demo-two-player.gif
index e3c4036..6b3f556 100644
Binary files a/website/static/gif/demo-two-player.gif and b/website/static/gif/demo-two-player.gif differ
diff --git a/website/static/gif/helper.gif b/website/static/gif/helper.gif
index e02ffe2..895c2e4 100644
Binary files a/website/static/gif/helper.gif and b/website/static/gif/helper.gif differ
diff --git a/website/static/gif/multiplayer.gif b/website/static/gif/multiplayer.gif
new file mode 100644
index 0000000..f1eda6d
Binary files /dev/null and b/website/static/gif/multiplayer.gif differ
diff --git a/website/static/gif/play_against_black_bot.gif b/website/static/gif/play_against_black_bot.gif
index 2b67df2..32d30a9 100644
Binary files a/website/static/gif/play_against_black_bot.gif and b/website/static/gif/play_against_black_bot.gif differ
diff --git a/website/static/gif/play_against_white_bot.gif b/website/static/gif/play_against_white_bot.gif
index 1929664..0fb7307 100644
Binary files a/website/static/gif/play_against_white_bot.gif and b/website/static/gif/play_against_white_bot.gif differ


Test patch: