From dddca6ed525deb61d27847f9babbf63f283bccbe Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:50:55 +0200
Subject: [PATCH 01/26] Add FXML views for home, players, leaderboard, and
games
Introduces new FXML files for the main home screen, players list, leaderboard, and recorded games views. These files define the UI structure and controllers for each section, enabling navigation and display of relevant content in the application.
---
src/main/resources/fxml/home.fxml | 62 ++++++++++++++++++++++
src/main/resources/fxml/leaderboard.fxml | 12 +++++
src/main/resources/fxml/players.fxml | 24 +++++++++
src/main/resources/fxml/recordedgames.fxml | 12 +++++
4 files changed, 110 insertions(+)
create mode 100644 src/main/resources/fxml/home.fxml
create mode 100644 src/main/resources/fxml/leaderboard.fxml
create mode 100644 src/main/resources/fxml/players.fxml
create mode 100644 src/main/resources/fxml/recordedgames.fxml
diff --git a/src/main/resources/fxml/home.fxml b/src/main/resources/fxml/home.fxml
new file mode 100644
index 0000000..c4b5ce9
--- /dev/null
+++ b/src/main/resources/fxml/home.fxml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/fxml/leaderboard.fxml b/src/main/resources/fxml/leaderboard.fxml
new file mode 100644
index 0000000..9f003d3
--- /dev/null
+++ b/src/main/resources/fxml/leaderboard.fxml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/fxml/players.fxml b/src/main/resources/fxml/players.fxml
new file mode 100644
index 0000000..368f5c6
--- /dev/null
+++ b/src/main/resources/fxml/players.fxml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/fxml/recordedgames.fxml b/src/main/resources/fxml/recordedgames.fxml
new file mode 100644
index 0000000..dc23334
--- /dev/null
+++ b/src/main/resources/fxml/recordedgames.fxml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From f6a4a127583434030683b1ef50b1e04936f8933a Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:51:25 +0200
Subject: [PATCH 02/26] Add controllers and home page CSS for new features
Introduced LeaderboardController and RecordedGamesController JavaFX controllers with initial structure. Added a comprehensive home.css stylesheet to style the application's home page, including transparency fixes, tab and card styling, and modern scrollbar appearance.
---
.../presentation/LeaderboardController.java | 26 +++
.../presentation/RecordedGamesController.java | 26 +++
src/main/resources/css/features/home.css | 202 ++++++++++++++++++
3 files changed, 254 insertions(+)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java
create mode 100644 src/main/resources/css/features/home.css
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
new file mode 100644
index 0000000..9883f11
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
@@ -0,0 +1,26 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/javafx/FXMLController.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.presentation;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.fxml.Initializable;
+
+/**
+ * FXML Controller class
+ *
+ * @author moham
+ */
+public class LeaderboardController implements Initializable {
+
+ /**
+ * Initializes the controller class.
+ */
+ @Override
+ public void initialize(URL url, ResourceBundle rb) {
+ // TODO
+ }
+
+}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java
new file mode 100644
index 0000000..1554de2
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java
@@ -0,0 +1,26 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/javafx/FXMLController.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.presentation;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.fxml.Initializable;
+
+/**
+ * FXML Controller class
+ *
+ * @author moham
+ */
+public class RecordedGamesController implements Initializable {
+
+ /**
+ * Initializes the controller class.
+ */
+ @Override
+ public void initialize(URL url, ResourceBundle rb) {
+ // TODO
+ }
+
+}
diff --git a/src/main/resources/css/features/home.css b/src/main/resources/css/features/home.css
new file mode 100644
index 0000000..b18730a
--- /dev/null
+++ b/src/main/resources/css/features/home.css
@@ -0,0 +1,202 @@
+/* home.css - The "Nuclear" Transparency Fix */
+
+/* 1. ROOT & GLOBAL DEFAULTS */
+.root {
+ -fx-background-color: #111022;
+ -fx-font-family: "Noto Sans", "Segoe UI", sans-serif;
+ -fx-text-fill: white;
+ -fx-focus-color: transparent;
+ -fx-faint-focus-color: transparent;
+}
+
+.label {
+ -fx-text-fill: white;
+}
+
+/* 2. SCROLL PANE (The Big White Box Fix) */
+/* We target the scroll-pane AND the viewport inside it */
+.scroll-pane,
+.scroll-pane .viewport,
+.scroll-pane .content {
+ -fx-background-color: transparent !important;
+ -fx-background-insets: 0;
+ -fx-padding: 0;
+}
+
+/* Also target your specific class just in case */
+.edge-to-edge,
+.edge-to-edge .viewport,
+.edge-to-edge .content {
+ -fx-background-color: transparent !important;
+}
+
+/* 3. TAB PANE (The Header Bar Fix) */
+.flat-tabs {
+ -fx-background-color: transparent;
+}
+
+/* The container for the tab buttons */
+.flat-tabs .tab-header-area {
+ -fx-background-color: transparent !important;
+ -fx-padding: 0;
+}
+
+/* The actual background bar behind the tabs - usually the culprit */
+.flat-tabs .tab-header-background {
+ -fx-background-color: transparent !important;
+ -fx-background-insets: 0;
+}
+
+.flat-tabs .headers-region {
+ -fx-background-color: transparent !important;
+}
+
+/* The main content area inside the tab */
+.flat-tabs .tab-content-area {
+ -fx-background-color: transparent !important;
+ -fx-padding: 0;
+ -fx-background-insets: 0;
+}
+
+/* 4. TAB BUTTONS */
+.flat-tabs .tab {
+ -fx-background-color: transparent;
+ -fx-background-insets: 0;
+ -fx-background-radius: 0;
+ -fx-padding: 15;
+ -fx-border-width: 0 0 2 0;
+ -fx-border-color: transparent;
+}
+
+.flat-tabs .tab-label {
+ -fx-text-fill: #94a3b8;
+ -fx-font-weight: bold;
+ -fx-font-size: 14px;
+}
+
+.flat-tabs .tab:hover {
+ -fx-background-color: rgba(255, 255, 255, 0.05);
+}
+
+.flat-tabs .tab:selected {
+ -fx-background-color: transparent !important;
+ -fx-border-color: transparent transparent #463eef transparent;
+}
+
+.flat-tabs .tab:selected .tab-label {
+ -fx-text-fill: #463eef !important;
+}
+
+/* 5. PLAYER CARDS */
+.player-card {
+ -fx-background-color: #1c1b2e;
+ -fx-background-radius: 16;
+ -fx-border-color: rgba(255,255,255,0.05);
+ -fx-border-radius: 16;
+ -fx-padding: 10;
+ -fx-cursor: hand;
+}
+
+.player-card:hover {
+ -fx-background-color: #252438 !important;
+ -fx-border-color: #463eef !important;
+}
+
+.player-name { -fx-text-fill: white; -fx-font-size: 15px; -fx-font-weight: bold; }
+.player-subtext { -fx-text-fill: #94a3b8; -fx-font-size: 11px; }
+
+/* 6. TOP BAR */
+.top-bar {
+ -fx-background-color: rgba(28, 27, 46, 0.95);
+ -fx-padding: 20;
+ -fx-border-color: rgba(255, 255, 255, 0.1);
+ -fx-border-width: 0 0 1 0;
+}
+
+/* ... (Keep your button styles for .signout-btn, .quick-match-btn, etc. as they were working fine) ... */
+.app-icon { -fx-text-fill: #463eef; -fx-font-size: 24px; -fx-font-weight: bold; }
+.app-title { -fx-text-fill: white; -fx-font-size: 20px; -fx-font-weight: 700; }
+.username { -fx-text-fill: white !important; -fx-font-weight: 700; -fx-font-size: 14px; }
+.user-level { -fx-text-fill: #94a3b8; -fx-font-size: 11px; }
+.avatar-profile { -fx-stroke: #463eef; -fx-stroke-width: 2; -fx-fill: #2d2d44; }
+
+.signout-btn {
+ -fx-background-color: #1c1b2e;
+ -fx-text-fill: #cbd5e1;
+ -fx-font-weight: bold;
+ -fx-background-radius: 8;
+ -fx-border-color: rgba(255,255,255,0.1);
+ -fx-border-radius: 8;
+ -fx-cursor: hand;
+ -fx-padding: 10;
+}
+.signout-btn:hover { -fx-background-color: #2a2940; -fx-border-color: rgba(255,255,255,0.2); }
+
+.quick-match-btn {
+ -fx-background-color: #463eef;
+ -fx-text-fill: white;
+ -fx-font-size: 15px;
+ -fx-font-weight: bold;
+ -fx-background-radius: 30;
+ -fx-padding: 15;
+ -fx-effect: dropshadow(gaussian, rgba(70, 62, 239, 0.6), 15, 0, 0, 4);
+ -fx-cursor: hand;
+}
+.quick-match-btn:hover { -fx-background-color: #6d66f5; }
+.btn-challenge { -fx-background-color: #463eef; -fx-text-fill: white; -fx-font-weight: bold; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8; }
+.btn-challenge:hover { -fx-background-color: #6d66f5; }
+.icon-btn { -fx-background-color: transparent; -fx-text-fill: #94a3b8; -fx-font-size: 16px; -fx-cursor: hand; }
+.icon-btn:hover { -fx-text-fill: white; -fx-background-color: rgba(255,255,255,0.1); -fx-background-radius: 8; }
+.btn-spectate { -fx-background-color: transparent; -fx-text-fill: #cbd5e1; -fx-border-color: rgba(255,255,255,0.2); -fx-border-radius: 8; -fx-padding: 8; -fx-font-weight: bold; }
+.btn-spectate:hover { -fx-background-color: rgba(255,255,255,0.05); -fx-text-fill: white; }
+.btn-disabled { -fx-background-color: transparent; -fx-text-fill: #475569; -fx-padding: 8; -fx-border-color: rgba(255,255,255,0.05); -fx-border-radius: 8; }
+/* Status Indicators */
+.status-text-online { -fx-text-fill: #00ff9d; -fx-font-weight: bold; -fx-font-size: 10px; }
+.status-text-ingame { -fx-text-fill: #ff9e00; -fx-font-weight: bold; -fx-font-size: 10px; }
+.status-text-busy { -fx-text-fill: #64748b; -fx-font-weight: bold; -fx-font-size: 10px; }
+.status-dot-online { -fx-fill: #00ff9d; }
+.status-dot-ingame { -fx-fill: #ff9e00; }
+.status-dot-busy { -fx-fill: #64748b; }
+
+/* --- SCROLLBAR STYLING (The Pro Look) --- */
+
+/* The track (background) */
+.modern-scroll .viewport {
+ -fx-background-color: transparent;
+}
+
+.modern-scroll .scroll-bar:vertical {
+ -fx-background-color: transparent;
+ -fx-pref-width: 12;
+}
+
+.modern-scroll .scroll-bar:vertical .track {
+ -fx-background-color: rgba(255, 255, 255, 0.05);
+ -fx-background-radius: 10;
+}
+
+.modern-scroll .scroll-bar:vertical .thumb {
+ -fx-background-color: #463eef; /* Your theme color */
+ -fx-background-radius: 10;
+ -fx-background-insets: 2;
+}
+
+.modern-scroll .scroll-bar:vertical .thumb:hover {
+ -fx-background-color: #6d66f5;
+}
+
+/* Hide the increment/decrement buttons */
+.modern-scroll .scroll-bar .increment-button,
+.modern-scroll .scroll-bar .decrement-button {
+ -fx-visible: false;
+ -fx-padding: 0;
+ -fx-pref-height: 0;
+}
+
+/* --- RESPONSIVE TWEAKS --- */
+.player-card {
+ /* Ensure the card has a minimum height but stretches width */
+ -fx-min-height: 70;
+ -fx-pref-width: 100%;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4);
+}
\ No newline at end of file
From 8aa815f1b1f3859af607d1ea9a3993d09809cd79 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:51:45 +0200
Subject: [PATCH 03/26] Add player and game request models, update Screens enum
Introduces PlayerEntity, GameRequestInfo, and AvailablePlayersInfo model classes to represent player and game request data. Also adds a 'home' screen to the Screens enum for navigation.
---
.../domain/managers/navigation/Screens.java | 3 +-
.../domain/model/AvailablePlayersInfo.java | 49 +++++++++++++++++++
.../domain/model/GameRequestInfo.java | 37 ++++++++++++++
.../domain/model/PlayerEntity.java | 45 +++++++++++++++++
4 files changed, 133 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
index d079f38..783f2b7 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
@@ -5,7 +5,8 @@ public enum Screens {
SECONDARY("secondary"),
SETTINGS("settings"),
SERVER_CONNECTION("server_connection"),
- AUTHENTICATION("authentication");
+ AUTHENTICATION("authentication"),
+ Home("home");
private final String name;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java
new file mode 100644
index 0000000..cbc1abb
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java
@@ -0,0 +1,49 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+import java.util.Vector;
+
+/**
+ *
+ * @author mahmoud
+ */
+public class AvailablePlayersInfo {
+ public Vector onlinePlayers;
+ public Vector inGamePlayers;
+ public Vector pendingPlayers;
+
+ public AvailablePlayersInfo(Vector online, Vector inGame, Vector pending) {
+ this.onlinePlayers = online;
+ this.inGamePlayers = inGame;
+ this.pendingPlayers = pending;
+ }
+
+ public Vector getOnlinePlayers() {
+ return onlinePlayers;
+ }
+
+ public void setOnlinePlayers(Vector onlinePlayers) {
+ this.onlinePlayers = onlinePlayers;
+ }
+
+ public Vector getInGamePlayers() {
+ return inGamePlayers;
+ }
+
+ public void setInGamePlayers(Vector inGamePlayers) {
+ this.inGamePlayers = inGamePlayers;
+ }
+
+ public Vector getPendingPlayers() {
+ return pendingPlayers;
+ }
+
+ public void setPendingPlayers(Vector pendingPlayers) {
+ this.pendingPlayers = pendingPlayers;
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java
new file mode 100644
index 0000000..ad7e617
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java
@@ -0,0 +1,37 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+/**
+ *
+ * @author mahmoud
+ */
+public class GameRequestInfo {
+ private final String requesterId;
+ private final String requesterUserName;
+ private final String targetId;
+
+ public GameRequestInfo(String requesterId, String requesterUserName, String targetId) {
+ this.requesterId = requesterId;
+ this.requesterUserName = requesterUserName;
+ this.targetId = targetId;
+ }
+
+ public String getRequesterUserName() {
+ return requesterUserName;
+ }
+
+
+
+
+ public String getRequesterId() {
+ return requesterId;
+ }
+
+ public String getTargetId() {
+ return targetId;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java
new file mode 100644
index 0000000..1c769ae
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java
@@ -0,0 +1,45 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+/**
+ *
+ * @author mahmoud
+ */
+
+public class PlayerEntity {
+ private String id;
+ private String username;
+ private int score;
+
+ public PlayerEntity(String id, String username, int score) {
+ this.id = id;
+ this.username = username;
+ this.score = score;
+ }
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public int getScore() {
+ return score;
+ }
+
+ public void setScore(int score) {
+ this.score = score;
+ }
+}
From bb01cbf56084ae74f2340844e2bf07cd364f86cd Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:52:33 +0200
Subject: [PATCH 04/26] Add domain models for game response, start, and move
Introduced GameResponseInfo, GameStartInfo, and MoveInfo classes in the domain model to represent game response details, game start information, and move data for the Tic Tac Toe client.
---
.../domain/model/GameResponseInfo.java | 58 +++++++++++++++++++
.../domain/model/GameStartInfo.java | 34 +++++++++++
.../domain/model/MoveInfo.java | 34 +++++++++++
3 files changed, 126 insertions(+)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java
new file mode 100644
index 0000000..5db33b2
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java
@@ -0,0 +1,58 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+/**
+ *
+ * @author mahmoud
+ */
+public class GameResponseInfo {
+ private String requesterId;
+ private String responderId;
+ private String responderUserName;
+ private boolean accepted;
+
+ public String getRequesterId() {
+ return requesterId;
+ }
+
+ public GameResponseInfo(String requesterId, String responderId, String responderUserName, boolean accepted) {
+ this.requesterId = requesterId;
+ this.responderId = responderId;
+ this.responderUserName = responderUserName;
+ this.accepted = accepted;
+ }
+
+ public String getResponderUserName() {
+ return responderUserName;
+ }
+
+ public void setResponderUserName(String responderUserName) {
+ this.responderUserName = responderUserName;
+ }
+
+
+ public void setRequesterId(String requesterId) {
+ this.requesterId = requesterId;
+ }
+
+ public String getResponderId() {
+ return responderId;
+ }
+
+ public void setResponderId(String responderId) {
+ this.responderId = responderId;
+ }
+
+ public boolean isAccepted() {
+ return accepted;
+ }
+
+ public void setAccepted(boolean accepted) {
+ this.accepted = accepted;
+ }
+
+
+}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java
new file mode 100644
index 0000000..1a3110f
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java
@@ -0,0 +1,34 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+/**
+ *
+ * @author moham
+ */
+public class GameStartInfo {
+ private final String roomId;
+ private final String player1;
+ private final String player2;
+
+ public GameStartInfo(String roomId, String player1, String player2) {
+ this.roomId = roomId;
+ this.player1 = player1;
+ this.player2 = player2;
+ }
+
+ public String getRoomId() {
+ return roomId;
+ }
+
+ public String getPlayer1() {
+ return player1;
+ }
+
+ public String getPlayer2() {
+ return player2;
+ }
+
+}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
new file mode 100644
index 0000000..9003782
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
@@ -0,0 +1,34 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.model;
+
+/**
+ *
+ * @author mahmoud
+ */
+public class MoveInfo {
+ private final String roomId;
+ private final String playerId;
+ private final Object move;
+
+ public MoveInfo(String playerId, Object move, String roomId) {
+ this.playerId = playerId;
+ this.move = move;
+ this.roomId = roomId;
+ }
+
+ public String getRoomId() {
+ return roomId;
+ }
+
+ public String getPlayerId() {
+ return playerId;
+ }
+
+ public Object getMove() {
+ return move;
+ }
+
+}
From 8c26f736caa52ba85f7e224d8655e731dd20f41e Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:52:51 +0200
Subject: [PATCH 05/26] Enhance server communication and error handling
Added new actions and error codes to the Action enum for improved server-client communication. Updated ServerConnectionManager to handle intentional disconnects and server connection loss with user alerts and navigation. Extended MessageRouter to support new actions, event handling, and error reporting, including integration with HomeController for UI updates.
---
.../network/ServerConnectionManager.java | 31 ++++++-
.../domain/services/communication/Action.java | 15 +++-
.../services/communication/MessageRouter.java | 90 +++++++++++++++++--
3 files changed, 127 insertions(+), 9 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
index cd3051c..7038411 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
@@ -4,15 +4,20 @@
*/
package com.boredxgames.tictactoeclient.domain.network;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
import com.boredxgames.tictactoeclient.domain.services.communication.Message;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter;
+import com.boredxgames.tictactoeclient.presentation.AuthenticationController;
import com.google.gson.Gson;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
+import javafx.application.Platform;
/**
*
@@ -26,6 +31,7 @@ public class ServerConnectionManager {
private Thread th;
private Gson gson = new Gson();
private AuthResponseEntity player ;
+ private volatile boolean isIntentionalDisconnect = false;
public void setPlayer(AuthResponseEntity player) {
this.player = player;
@@ -45,6 +51,7 @@ public static ServerConnectionManager getInstance() {
}
public void connect(String host, int port ) throws IOException {
+ isIntentionalDisconnect = false;
InetAddress ip = InetAddress.getByName("localhost");
socket = new Socket(ip, port);
dis = new DataInputStream(socket.getInputStream());
@@ -66,12 +73,31 @@ private void readMessages() {
MessageRouter router = MessageRouter.getInstance();
router.navigateMessage(response);
} catch (IOException ex) {
+ if (isIntentionalDisconnect) {
+ System.out.println("Disconnected intentionally.");
+ break;
+ }
+
close();
+
+ Platform.runLater(() -> {
+ AuthenticationController.showUserAlert("Connection to server lost.");
+ NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE);
+ });
+ break;
}
}
}
+ public void disconnect() {
+ try {
+ isIntentionalDisconnect = true;
+ close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
public synchronized void sendMessage(Message msg) {
try {
@@ -84,6 +110,9 @@ public synchronized void sendMessage(Message msg) {
}
}
+ public AuthResponseEntity getPlayer() {
+ return player;
+}
public void close() {
try {
@@ -101,4 +130,4 @@ public void close() {
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
index f277d26..fe68cc7 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
@@ -9,14 +9,23 @@ public enum Action {
LOGIN(10),
REGISTER(20),
REQUEST_GAME(30),
- GAME_RESPONSE(40),
+ GAME_RESPONSE(35),
+ GAME_START(40),
SEND_GAME_UPDATE(50),
+ GAME_END(55),
USERNAME_NOT_FOUND(60),
REGISTERATION_SUCCESS(70),
INTERNAL_SERVER_ERROR(80),
INVALID_CREDENTIAL(90),
LOGIN_SUCCESS(100),
- USERNAME_ALREADY_EXIST(110);
+ USERNAME_ALREADY_EXIST(110),
+ PLAYER_BUSY(120),
+ PENDING_REQUEST_EXISTS(130),
+ TARGET_HAS_PENDING_REQUEST(140),
+ ROOM_NOT_FOUND(150),
+ INVALID_OPPONENT(160),
+ NO_PENDING_REQUEST(170),
+ GET_AVAILABLE_PLAYERS(180);
final int id;
@@ -44,4 +53,4 @@ public String toString() {
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index d928c9a..99d4d8b 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -12,17 +12,24 @@
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
+import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameStartInfo;
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.LOGIN_SUCCESS;
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USERNAME_NOT_FOUND;
import com.boredxgames.tictactoeclient.presentation.AuthenticationController;
+import com.boredxgames.tictactoeclient.presentation.HomeController;
import com.google.gson.Gson;
+import javafx.application.Platform;
public class MessageRouter {
private static MessageRouter instance;
private static ServerConnectionManager connection;
private Gson gson = new Gson();
+ private static HomeController homeController;
private MessageRouter() {
connection = ServerConnectionManager.getInstance();
}
@@ -43,19 +50,21 @@ public void navigateMessage(String response) {
switch (messageType) {
case REQUEST -> responseMessage = handleRequest(message);
case RESPONSE -> handleResponse(message);
- case EVENT -> {
- }
+ case EVENT -> handleEvent(message);
case ERROR -> handleError(message);
default -> System.out.println("Unknown MessageType: " + messageType);
}
- // Handle event messages
}
+ public static void setHomeController(HomeController controller) {
+ homeController = controller;
+ }
private Message handleRequest(Message msg) {
Action action = msg.getHeader().getAction();
assert action != null;
return switch (action) {
+
default: {
System.out.println("Unknown Action: " + action);
@@ -74,8 +83,9 @@ private void handleResponse(Message msg) {
{
System.out.println("Login Success");
AuthResponseEntity responseData = gson.fromJson(msg.getData(),AuthResponseEntity.class);
+ ServerConnectionManager.getInstance().setPlayer(responseData);
System.out.println(responseData);
- NavigationManager.navigate(Screens.SECONDARY, NavigationAction.REPLACE);
+ NavigationManager.navigate(Screens.Home, NavigationAction.REPLACE);
}
case USERNAME_NOT_FOUND->{
@@ -83,6 +93,44 @@ private void handleResponse(Message msg) {
AuthenticationController.showUserAlert("Username not found");
}
+ case GET_AVAILABLE_PLAYERS -> {
+ AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.updatePlayersList(info));
+ }
+ }
+ case GAME_RESPONSE -> {
+ GameResponseInfo info = gson.fromJson(msg.getData(), GameResponseInfo.class);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.handleServerGameResponse(info));
+ }
+ }
+
+ default -> {
+ System.out.println("Unknown Action: " + action);
+
+ }
+ };
+ }
+
+ private void handleEvent(Message msg) {
+ Action action = msg.getHeader().getAction();
+ System.out.println(action);
+
+ switch (action) {
+ case REQUEST_GAME -> {
+ GameRequestInfo info = gson.fromJson(msg.getData(), GameRequestInfo.class);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showIncomingGameRequest(info));
+ }
+
+ }
+ case GAME_START -> {
+ GameStartInfo info = gson.fromJson(msg.getData(), GameStartInfo.class);
+ NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE);
+
+ }
+
default -> {
System.out.println("Unknown Action: " + action);
@@ -102,10 +150,42 @@ private void handleError(Message msg) {
}
case INTERNAL_SERVER_ERROR->{
+ String errorMessage = "Server error. Please try again.";
System.out.println("Internal Server Error");
AuthenticationController.showUserAlert("Internal Server Error");
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showErrorAlert(errorMessage));
+ }
}
+ case PLAYER_BUSY -> {
+ String errorMsg = "That player is currently in a game.";
+ System.out.println(errorMsg);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
+ }
+ }
+ case PENDING_REQUEST_EXISTS -> {
+ String errorMsg = "You already have a request pending.";
+ System.out.println(errorMsg);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
+ }
+ }
+ case TARGET_HAS_PENDING_REQUEST -> {
+ String errorMsg = "That player is considering another challenge.";
+ System.out.println(errorMsg);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
+ }
+ }
+ case ROOM_NOT_FOUND -> {
+ String errorMsg = "Game room no longer exists.";
+ System.out.println(errorMsg);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
+ }
+ }
default -> {
System.out.println("Unknown Action: " + action);
@@ -113,4 +193,4 @@ private void handleError(Message msg) {
};
}
-}
+}
\ No newline at end of file
From e8f667f0435d4e80a5f4c945de739d5cb3d5fab7 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sat, 10 Jan 2026 16:53:00 +0200
Subject: [PATCH 06/26] Add game service and player controllers
Introduced GameService for handling game requests and responses. Added HomeController and PlayersController to manage UI interactions, player lists, and game challenge flows in the Tic-Tac-Toe client.
---
.../domain/services/game/GameService.java | 82 ++++++++++++
.../presentation/HomeController.java | 121 ++++++++++++++++++
.../presentation/PlayersController.java | 119 +++++++++++++++++
3 files changed, 322 insertions(+)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
new file mode 100644
index 0000000..87bc8b8
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
@@ -0,0 +1,82 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.boredxgames.tictactoeclient.domain.services.game;
+
+/**
+ *
+ * @author mahmoud
+ */
+
+
+import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
+import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
+import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
+import com.boredxgames.tictactoeclient.domain.services.communication.Action;
+import com.boredxgames.tictactoeclient.domain.services.communication.Message;
+import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
+
+public class GameService {
+
+ private static GameService instance;
+ private final ServerConnectionManager connection;
+ private boolean isWaitingForResponse = false;
+
+ private GameService() {
+ this.connection = ServerConnectionManager.getInstance();
+ }
+
+ public static synchronized GameService getInstance() {
+ if (instance == null) {
+ instance = new GameService();
+ }
+ return instance;
+ }
+
+ public boolean isWaiting() {
+ return isWaitingForResponse;
+ }
+
+ public void setWaiting(boolean waiting) {
+ this.isWaitingForResponse = waiting;
+ System.out.println("DEBUG: User waiting state set to: " + waiting);
+ }
+
+ public void requestAvailablePlayers() {
+ Message msg = Message.createMessage(MessageType.REQUEST, Action.GET_AVAILABLE_PLAYERS, "");
+ connection.sendMessage(msg);
+ }
+
+ public void sendGameRequest(String targetPlayerId) {
+ if (isWaitingForResponse) {
+ System.out.println("Blocked: Already waiting for a response.");
+ return;
+ }
+
+ AuthResponseEntity me = connection.getPlayer();
+ if (me == null) return;
+
+ GameRequestInfo requestInfo = new GameRequestInfo(me.getId(), me.getUserName(), targetPlayerId);
+ Message msg = Message.createMessage(MessageType.REQUEST, Action.REQUEST_GAME, requestInfo);
+
+ System.out.println("Sending Game Request to: " + targetPlayerId);
+ connection.sendMessage(msg);
+ setWaiting(true);
+ }
+
+ public void sendGameResponse(GameRequestInfo originalRequest, boolean accepted) {
+ AuthResponseEntity me = connection.getPlayer();
+
+ GameResponseInfo responseInfo = new GameResponseInfo(
+ originalRequest.getRequesterId(),
+ originalRequest.getTargetId(),
+ me.getUserName(),
+ accepted
+ );
+
+ Message msg = Message.createMessage(MessageType.RESPONSE, Action.GAME_RESPONSE, responseInfo);
+ connection.sendMessage(msg);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
new file mode 100644
index 0000000..dc46c96
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -0,0 +1,121 @@
+package com.boredxgames.tictactoeclient.presentation;
+
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
+import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
+import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo;
+import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
+import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
+import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter;
+import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+
+public class HomeController implements Initializable {
+
+ @FXML private BorderPane rootPane;
+ @FXML private Label usernameLabel;
+ @FXML private Label scoreLabel;
+ @FXML private PlayersController playersViewController;
+
+ @Override
+ public void initialize(URL url, ResourceBundle rb) {
+ MessageRouter.setHomeController(this);
+
+ AuthResponseEntity currentUser = ServerConnectionManager.getInstance().getPlayer();
+ System.out.println("currrrrent : "+currentUser.getUserName());
+ if (currentUser != null) {
+ if(usernameLabel != null) usernameLabel.setText(currentUser.getUserName());
+ if(scoreLabel != null) scoreLabel.setText("score " + currentUser.getScore() + " P");
+ }
+ Platform.runLater(() -> {
+ if (rootPane.getScene() != null) {
+ rootPane.getScene().setFill(javafx.scene.paint.Color.valueOf("#111022"));
+ }
+
+ javafx.scene.Node headerBackground = rootPane.lookup(".tab-header-background");
+ if (headerBackground != null) {
+ headerBackground.setStyle("-fx-background-color: transparent;");
+ }
+
+ javafx.scene.Node viewport = rootPane.lookup(".viewport");
+ if (viewport != null) {
+ viewport.setStyle("-fx-background-color: transparent;");
+ }
+ });
+ GameService.getInstance().requestAvailablePlayers();
+
+
+ }
+
+ public void updatePlayersList(AvailablePlayersInfo info) {
+ if (playersViewController != null) {
+ playersViewController.updateList(info);
+ }
+ }
+
+
+public void handleServerGameResponse(GameResponseInfo info) {
+ GameService.getInstance().setWaiting(false);
+
+ if (info.isAccepted()) {
+ System.out.println("Opponent Accepted Starting Game");
+
+ } else {
+ System.out.println("Opponent Declined.");
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Challenge Declined");
+ alert.setHeaderText(null);
+ alert.setContentText("Player " + info.getResponderUserName()+ " declined your challenge.");
+ alert.showAndWait();
+
+ GameService.getInstance().requestAvailablePlayers();
+ }
+ }
+
+ public void showIncomingGameRequest(GameRequestInfo info) {
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+ alert.setTitle("Incoming Challenge");
+ alert.setHeaderText("Game Request");
+ alert.setContentText(info.getRequesterUserName()+ " wants to play Tic-Tac-Toe!");
+
+ ButtonType acceptBtn = new ButtonType("Accept");
+ ButtonType declineBtn = new ButtonType("Decline");
+
+ alert.getButtonTypes().setAll(acceptBtn, declineBtn);
+
+ alert.showAndWait().ifPresent(type -> {
+ boolean accepted = (type == acceptBtn);
+ GameService.getInstance().sendGameResponse(info, accepted);
+ });
+ });
+ }
+
+ public void showErrorAlert(String message) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle("Error");
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
+ @FXML
+ private void onSignOut() {
+ ServerConnectionManager.getInstance().disconnect();
+
+ ServerConnectionManager.getInstance().setPlayer(null);
+
+ NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
new file mode 100644
index 0000000..04235ca
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
@@ -0,0 +1,119 @@
+package com.boredxgames.tictactoeclient.presentation;
+
+import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
+import com.boredxgames.tictactoeclient.domain.model.PlayerEntity;
+import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Circle;
+
+public class PlayersController {
+
+ @FXML
+ private VBox playersContainer;
+
+ public void updateList(AvailablePlayersInfo info) {
+ playersContainer.getChildren().clear();
+
+ if (info.getOnlinePlayers() != null) {
+ for (PlayerEntity p : info.getOnlinePlayers()) {
+ playersContainer.getChildren().add(createPlayerCard(p, "ONLINE", "online", true));
+ }
+ }
+
+ if (info.getInGamePlayers() != null) {
+ for (PlayerEntity p : info.getInGamePlayers()) {
+ playersContainer.getChildren().add(createPlayerCard(p, "IN GAME", "ingame", false));
+ }
+ }
+
+ if (info.getPendingPlayers() != null) {
+ for (PlayerEntity p : info.getPendingPlayers()) {
+ playersContainer.getChildren().add(createPlayerCard(p, "BUSY", "busy", false));
+ }
+ }
+
+ if (playersContainer.getChildren().isEmpty()) {
+ Label placeholder = new Label("No other players connected.");
+ placeholder.setStyle("-fx-text-fill: #64748b; -fx-font-size: 14px; -fx-padding: 20;");
+ playersContainer.getChildren().add(placeholder);
+ }
+ }
+
+ private HBox createPlayerCard(PlayerEntity p, String statusText, String type, boolean canChallenge) {
+ HBox card = new HBox(16);
+ card.getStyleClass().add("player-card");
+ card.setAlignment(Pos.CENTER_LEFT);
+
+ HBox.setHgrow(card, Priority.ALWAYS);
+ card.setMaxWidth(Double.MAX_VALUE);
+
+ StackPane avatarStack = new StackPane();
+ Circle avatarBg = new Circle(24);
+ avatarBg.setStyle("-fx-fill: #334155;");
+
+ Circle statusDot = new Circle(6);
+ statusDot.getStyleClass().add("status-dot-" + type);
+ StackPane.setAlignment(statusDot, Pos.BOTTOM_RIGHT);
+ avatarStack.getChildren().addAll(avatarBg, statusDot);
+
+ VBox infoBox = new VBox(2);
+ infoBox.setAlignment(Pos.CENTER_LEFT);
+
+ Label nameLabel = new Label(p.getUsername());
+ nameLabel.getStyleClass().add("player-name");
+
+ HBox statusLine = new HBox(6);
+ statusLine.setAlignment(Pos.CENTER_LEFT);
+ Label statusLabel = new Label(statusText);
+ statusLabel.getStyleClass().add("status-text-" + type);
+ Label eloLabel = new Label("• " + p.getScore() + " pts");
+ eloLabel.getStyleClass().add("player-subtext");
+
+ statusLine.getChildren().addAll(statusLabel, eloLabel);
+ infoBox.getChildren().addAll(nameLabel, statusLine);
+
+ Pane spacer = new Pane();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ Button actionBtn = new Button();
+
+ if (canChallenge) {
+ actionBtn.setText("Challenge ⚔");
+ actionBtn.getStyleClass().add("btn-challenge");
+
+ actionBtn.setOnAction(e -> {
+
+ if (GameService.getInstance().isWaiting()) {
+ Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setTitle("Please Wait");
+ alert.setHeaderText(null);
+ alert.setContentText("You already have a pending request. Please wait for the response.");
+ alert.showAndWait();
+ return;
+ }
+
+ actionBtn.setDisable(true);
+ actionBtn.setText("Sent...");
+ GameService.getInstance().sendGameRequest(p.getId());
+ });
+
+ } else {
+ actionBtn.setText(statusText);
+ actionBtn.getStyleClass().add("btn-disabled");
+ actionBtn.setDisable(true);
+ }
+
+ card.getChildren().addAll(avatarStack, infoBox, spacer, actionBtn);
+ return card;
+ }
+}
\ No newline at end of file
From c8368b3c58647767cbc9f5b6fcc78147ad7c3d6a Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Sat, 10 Jan 2026 21:30:23 +0200
Subject: [PATCH 07/26] Implement online game management with game state
handling and player move synchronization
---
pom.xml | 10 +--
.../com/boredxgames/tictactoeclient/App.java | 25 +++++-
.../services/{game => }/GameService.java | 4 +-
.../services/communication/MessageRouter.java | 10 ++-
.../domain/services/game/GameManager.java | 47 +++++++++++
.../services/game/OnlineGameManager.java | 77 +++++++++++++++++++
.../presentation/HomeController.java | 2 +-
.../presentation/PlayersController.java | 3 +-
src/main/java/module-info.java | 11 ++-
9 files changed, 169 insertions(+), 20 deletions(-)
rename src/main/java/com/boredxgames/tictactoeclient/domain/services/{game => }/GameService.java (95%)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
diff --git a/pom.xml b/pom.xml
index 363c2db..902e415 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,16 +26,16 @@
${javafx.version}
-
- org.openjfx
- javafx-fxml
- ${javafx.version}
-
org.json
json
20240303
+
+ org.openjfx
+ javafx-fxml
+ ${javafx.version}
+
diff --git a/src/main/java/com/boredxgames/tictactoeclient/App.java b/src/main/java/com/boredxgames/tictactoeclient/App.java
index 6fff00e..ffe94d9 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/App.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/App.java
@@ -9,11 +9,9 @@
public class App extends Application {
- private static Scene scene;
-
@Override
public void start(Stage stage) throws IOException {
- scene = NavigationManager.init();
+ Scene scene = NavigationManager.init();
ThemeManager.init(scene);
stage.setScene(scene);
stage.show();
@@ -23,3 +21,24 @@ public static void main(String[] args) {
launch();
}
}
+
+/*
+* 0- player_1 using player_id is x and starts the game
+* -- if my plauer_id != player_1 id then i'm blocked
+* 1- forward move
+* 2- in forward move send game status (winner, draw, In-Progress)
+* 3- winner -> update score for player
+*
+*
+*
+* */
+/*
+online game manager impls game manager
+
+game manager:
+- forward move
+- can play
+
+
+
+*/
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
similarity index 95%
rename from src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
rename to src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
index 87bc8b8..12723d8 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
@@ -2,7 +2,7 @@
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
-package com.boredxgames.tictactoeclient.domain.services.game;
+package com.boredxgames.tictactoeclient.domain.services;
/**
*
@@ -22,7 +22,7 @@ public class GameService {
private static GameService instance;
private final ServerConnectionManager connection;
- private boolean isWaitingForResponse = false;
+ private boolean isWaitingForResponse = false;
private GameService() {
this.connection = ServerConnectionManager.getInstance();
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index 99d4d8b..2d60056 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -19,6 +19,8 @@
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.LOGIN_SUCCESS;
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USERNAME_NOT_FOUND;
+
+import com.boredxgames.tictactoeclient.domain.services.game.OnlineGameManager;
import com.boredxgames.tictactoeclient.presentation.AuthenticationController;
import com.boredxgames.tictactoeclient.presentation.HomeController;
import com.google.gson.Gson;
@@ -46,9 +48,8 @@ public void navigateMessage(String response) {
MessageType messageType = message.getHeader().getMsgType();
assert messageType != null;
- Message responseMessage;
switch (messageType) {
- case REQUEST -> responseMessage = handleRequest(message);
+ case REQUEST -> handleRequest(message);
case RESPONSE -> handleResponse(message);
case EVENT -> handleEvent(message);
case ERROR -> handleError(message);
@@ -64,9 +65,12 @@ private Message handleRequest(Message msg) {
assert action != null;
return switch (action) {
+// case SEND_GAME_UPDATE ->
+// {
+// }
- default: {
+ default -> {
System.out.println("Unknown Action: " + action);
yield new Message();
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
new file mode 100644
index 0000000..861a3a9
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
@@ -0,0 +1,47 @@
+package com.boredxgames.tictactoeclient.domain.services.game;
+
+import com.boredxgames.tictactoeclient.domain.services.GameBoard;
+import com.boredxgames.tictactoeclient.domain.services.GameBoard.GameState;
+
+public abstract class GameManager {
+
+ protected GameBoard gameBoard;
+ protected GameStateListener listener;
+
+ public interface GameStateListener {
+ void onBoardUpdated();
+ void onGameOver(GameState state, char winner);
+ void onError(String message);
+ }
+
+ public GameManager(GameStateListener listener) {
+ this.listener = listener;
+ this.gameBoard = new GameBoard();
+ }
+
+ public abstract void makeMove(int row, int col);
+
+ public void resetGame() {
+ gameBoard.resetGame();
+ if (listener != null) {
+ listener.onBoardUpdated();
+ }
+ }
+
+ public GameBoard getGameBoard() {
+ return gameBoard;
+ }
+
+ public char getCurrentPlayer() {
+ return gameBoard.getCurrentPlayer();
+ }
+
+ protected void checkGameState() {
+ if (listener == null) return;
+
+ GameState state = gameBoard.getGameState();
+ if (state != GameState.IN_PROGRESS) {
+ listener.onGameOver(state, gameBoard.getWinner());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
new file mode 100644
index 0000000..25bb2c8
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
@@ -0,0 +1,77 @@
+package com.boredxgames.tictactoeclient.domain.services.game;
+
+import com.boredxgames.tictactoeclient.domain.model.MoveInfo;
+
+import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
+
+import com.boredxgames.tictactoeclient.domain.services.communication.Action;
+
+import com.boredxgames.tictactoeclient.domain.services.communication.Message;
+
+import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
+
+
+public class OnlineGameManager extends GameManager {
+
+ private final String roomId;
+ private final char localPlayerSymbol;
+ private final String playerId;
+
+ public OnlineGameManager(GameStateListener listener, String roomId, char localPlayerSymbol, String playerId) {
+ super(listener);
+ this.roomId = roomId;
+ this.localPlayerSymbol = localPlayerSymbol;
+ this.playerId = playerId;
+ }
+
+ @Override
+ public void makeMove(int row, int col) {
+ if (gameBoard.getCurrentPlayer() != localPlayerSymbol) {
+ if (listener != null) {
+ listener.onError("It is not your turn!");
+ }
+ return;
+ }
+
+ if (!gameBoard.isValidMove(row, col)) {
+ if (listener != null) listener.onError("Invalid Move");
+ return;
+ }
+
+ int[] moveData = new int[]{row, col};
+ MoveInfo moveInfo = new MoveInfo(playerId, moveData, roomId);
+ Message message = Message.createMessage(MessageType.REQUEST, Action.SEND_GAME_UPDATE, moveInfo);
+
+ ServerConnectionManager.getInstance().sendMessage(message);
+
+ boolean success = gameBoard.makeMove(row, col, localPlayerSymbol);
+ if (success) {
+ if (listener != null) {
+ listener.onBoardUpdated();
+ checkGameState();
+ }
+ }
+ }
+
+ public void onNetworkMoveReceived(int row, int col, char playerSymbol) {
+ if (gameBoard.getCellValue(row, col) == playerSymbol) {
+ return;
+ }
+
+ boolean success = gameBoard.makeMove(row, col, playerSymbol);
+
+ if (success) {
+ if (listener != null) {
+ listener.onBoardUpdated();
+ checkGameState();
+ }
+ } else {
+ if (listener != null) listener.onError("Game desynchronized.");
+ }
+ }
+
+ @Override
+ public void resetGame() {
+ throw new UnsupportedOperationException("Can't reset an online game.");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index dc46c96..73aa210 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -9,7 +9,7 @@
import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter;
-import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
import java.net.URL;
import java.util.ResourceBundle;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
index 04235ca..a49f857 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
@@ -2,8 +2,7 @@
import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
import com.boredxgames.tictactoeclient.domain.model.PlayerEntity;
-import com.boredxgames.tictactoeclient.domain.services.game.GameService;
-import javafx.application.Platform;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 0ac78aa..05121f8 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -4,12 +4,15 @@
requires org.json;
requires com.google.gson;
requires java.base;
- opens com.boredxgames.tictactoeclient to javafx.fxml;
- opens com.boredxgames.tictactoeclient.domain.services.communication to com.google.gson;
- opens com.boredxgames.tictactoeclient.domain.model to com.google.gson;
+
exports com.boredxgames.tictactoeclient;
exports com.boredxgames.tictactoeclient.presentation;
- opens com.boredxgames.tictactoeclient.presentation to javafx.fxml;
exports com.boredxgames.tictactoeclient.domain.managers.state;
exports com.boredxgames.tictactoeclient.domain.managers.state.records;
+
+ opens com.boredxgames.tictactoeclient.presentation to javafx.fxml;
+ opens com.boredxgames.tictactoeclient to com.google.gson, javafx.fxml;
+ opens com.boredxgames.tictactoeclient.domain.model to com.google.gson;
+ opens com.boredxgames.tictactoeclient.domain.services.communication to com.google.gson;
+
}
From ae4235b29f1be682412daf8d34860619e79c5095 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:53:07 +0200
Subject: [PATCH 08/26] Redesign home and leaderboard FXML layouts
Refactored home.fxml to use a StackPane root with a background pane and nested BorderPane for main content. Updated leaderboard.fxml with a modernized layout, including a styled leaderboard card, scrollable player list, and a footer for the current user's rank and score.
---
src/main/resources/fxml/home.fxml | 96 ++++++++++++------------
src/main/resources/fxml/leaderboard.fxml | 66 ++++++++++++++--
2 files changed, 106 insertions(+), 56 deletions(-)
diff --git a/src/main/resources/fxml/home.fxml b/src/main/resources/fxml/home.fxml
index c4b5ce9..9bc7274 100644
--- a/src/main/resources/fxml/home.fxml
+++ b/src/main/resources/fxml/home.fxml
@@ -11,52 +11,52 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/fxml/leaderboard.fxml b/src/main/resources/fxml/leaderboard.fxml
index 9f003d3..6ac686b 100644
--- a/src/main/resources/fxml/leaderboard.fxml
+++ b/src/main/resources/fxml/leaderboard.fxml
@@ -1,12 +1,62 @@
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
+
+
+
+
+
+
\ No newline at end of file
From 6e2b211102369dce26809ab7f68660a134759e36 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:53:13 +0200
Subject: [PATCH 09/26] Update home.css
---
src/main/resources/css/features/home.css | 105 +++++++++++++++--------
1 file changed, 71 insertions(+), 34 deletions(-)
diff --git a/src/main/resources/css/features/home.css b/src/main/resources/css/features/home.css
index b18730a..f264ef9 100644
--- a/src/main/resources/css/features/home.css
+++ b/src/main/resources/css/features/home.css
@@ -1,24 +1,21 @@
-/* home.css - The "Nuclear" Transparency Fix */
-/* 1. ROOT & GLOBAL DEFAULTS */
.root {
-fx-background-color: #111022;
-fx-font-family: "Noto Sans", "Segoe UI", sans-serif;
-fx-text-fill: white;
- -fx-focus-color: transparent;
- -fx-faint-focus-color: transparent;
+ -fx-focus-color: rgba(0,0,0,0);
+ -fx-faint-focus-color: rgba(0,0,0,0);
}
.label {
-fx-text-fill: white;
}
-/* 2. SCROLL PANE (The Big White Box Fix) */
-/* We target the scroll-pane AND the viewport inside it */
+
.scroll-pane,
.scroll-pane .viewport,
.scroll-pane .content {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
-fx-background-insets: 0;
-fx-padding: 0;
}
@@ -27,45 +24,41 @@
.edge-to-edge,
.edge-to-edge .viewport,
.edge-to-edge .content {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
}
-/* 3. TAB PANE (The Header Bar Fix) */
.flat-tabs {
- -fx-background-color: transparent;
+ -fx-background-color: rgba(0,0,0,0);
}
-/* The container for the tab buttons */
.flat-tabs .tab-header-area {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
-fx-padding: 0;
}
-/* The actual background bar behind the tabs - usually the culprit */
.flat-tabs .tab-header-background {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
-fx-background-insets: 0;
}
.flat-tabs .headers-region {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
}
-/* The main content area inside the tab */
.flat-tabs .tab-content-area {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
-fx-padding: 0;
-fx-background-insets: 0;
}
/* 4. TAB BUTTONS */
.flat-tabs .tab {
- -fx-background-color: transparent;
+ -fx-background-color: rgba(0,0,0,0);
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-padding: 15;
-fx-border-width: 0 0 2 0;
- -fx-border-color: transparent;
+ -fx-border-color: rgba(0,0,0,0);
}
.flat-tabs .tab-label {
@@ -79,7 +72,7 @@
}
.flat-tabs .tab:selected {
- -fx-background-color: transparent !important;
+ -fx-background-color: rgba(0,0,0,0) !important;
-fx-border-color: transparent transparent #463eef transparent;
}
@@ -87,19 +80,21 @@
-fx-text-fill: #463eef !important;
}
-/* 5. PLAYER CARDS */
.player-card {
-fx-background-color: #1c1b2e;
-fx-background-radius: 16;
-fx-border-color: rgba(255,255,255,0.05);
+ -fx-border-width: 2px;
-fx-border-radius: 16;
-fx-padding: 10;
-fx-cursor: hand;
+ -fx-transition: -fx-background-color 0.2s, -fx-border-color 0.2s;
}
.player-card:hover {
-fx-background-color: #252438 !important;
-fx-border-color: #463eef !important;
+ -fx-border-width: 2px; /* Must match non-hover width */
}
.player-name { -fx-text-fill: white; -fx-font-size: 15px; -fx-font-weight: bold; }
@@ -113,7 +108,6 @@
-fx-border-width: 0 0 1 0;
}
-/* ... (Keep your button styles for .signout-btn, .quick-match-btn, etc. as they were working fine) ... */
.app-icon { -fx-text-fill: #463eef; -fx-font-size: 24px; -fx-font-weight: bold; }
.app-title { -fx-text-fill: white; -fx-font-size: 20px; -fx-font-weight: 700; }
.username { -fx-text-fill: white !important; -fx-font-weight: 700; -fx-font-size: 14px; }
@@ -143,13 +137,17 @@
-fx-cursor: hand;
}
.quick-match-btn:hover { -fx-background-color: #6d66f5; }
+
.btn-challenge { -fx-background-color: #463eef; -fx-text-fill: white; -fx-font-weight: bold; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8; }
.btn-challenge:hover { -fx-background-color: #6d66f5; }
-.icon-btn { -fx-background-color: transparent; -fx-text-fill: #94a3b8; -fx-font-size: 16px; -fx-cursor: hand; }
+
+.icon-btn { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #94a3b8; -fx-font-size: 16px; -fx-cursor: hand; }
.icon-btn:hover { -fx-text-fill: white; -fx-background-color: rgba(255,255,255,0.1); -fx-background-radius: 8; }
-.btn-spectate { -fx-background-color: transparent; -fx-text-fill: #cbd5e1; -fx-border-color: rgba(255,255,255,0.2); -fx-border-radius: 8; -fx-padding: 8; -fx-font-weight: bold; }
+
+.btn-spectate { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #cbd5e1; -fx-border-color: rgba(255,255,255,0.2); -fx-border-radius: 8; -fx-padding: 8; -fx-font-weight: bold; }
.btn-spectate:hover { -fx-background-color: rgba(255,255,255,0.05); -fx-text-fill: white; }
-.btn-disabled { -fx-background-color: transparent; -fx-text-fill: #475569; -fx-padding: 8; -fx-border-color: rgba(255,255,255,0.05); -fx-border-radius: 8; }
+.btn-disabled { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #475569; -fx-padding: 8; -fx-border-color: rgba(255,255,255,0.05); -fx-border-radius: 8; }
+
/* Status Indicators */
.status-text-online { -fx-text-fill: #00ff9d; -fx-font-weight: bold; -fx-font-size: 10px; }
.status-text-ingame { -fx-text-fill: #ff9e00; -fx-font-weight: bold; -fx-font-size: 10px; }
@@ -158,15 +156,13 @@
.status-dot-ingame { -fx-fill: #ff9e00; }
.status-dot-busy { -fx-fill: #64748b; }
-/* --- SCROLLBAR STYLING (The Pro Look) --- */
-
-/* The track (background) */
+/* --- SCROLLBAR STYLING --- */
.modern-scroll .viewport {
- -fx-background-color: transparent;
+ -fx-background-color: rgba(0,0,0,0);
}
.modern-scroll .scroll-bar:vertical {
- -fx-background-color: transparent;
+ -fx-background-color: rgba(0,0,0,0);
-fx-pref-width: 12;
}
@@ -176,7 +172,7 @@
}
.modern-scroll .scroll-bar:vertical .thumb {
- -fx-background-color: #463eef; /* Your theme color */
+ -fx-background-color: #463eef;
-fx-background-radius: 10;
-fx-background-insets: 2;
}
@@ -185,7 +181,6 @@
-fx-background-color: #6d66f5;
}
-/* Hide the increment/decrement buttons */
.modern-scroll .scroll-bar .increment-button,
.modern-scroll .scroll-bar .decrement-button {
-fx-visible: false;
@@ -195,8 +190,50 @@
/* --- RESPONSIVE TWEAKS --- */
.player-card {
- /* Ensure the card has a minimum height but stretches width */
-fx-min-height: 70;
-fx-pref-width: 100%;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4);
-}
\ No newline at end of file
+}
+
+/* --- LEADERBOARD SPECIFIC STYLES --- */
+
+.leaderboard-card {
+ -fx-background-color: #0f0e1e; /* Slightly darker than root */
+ -fx-background-radius: 16;
+ -fx-border-color: rgba(255,255,255,0.05);
+ -fx-border-radius: 16;
+ -fx-padding: 0;
+}
+
+.leaderboard-header-row {
+ -fx-padding: 15 ;
+ -fx-border-color: rgba(255,255,255,0.05);
+ -fx-border-width: 0 0 1 0;
+}
+
+.column-header {
+ -fx-text-fill: #64748b; /* Muted text for headers */
+ -fx-font-size: 11px;
+ -fx-font-weight: bold;
+ -fx-letter-spacing: 1px;
+}
+
+.leaderboard-row {
+ -fx-padding: 15 ;
+ -fx-background-color: transparent;
+ -fx-border-width: 0 0 1 0;
+ -fx-border-color: rgba(255,255,255,0.02);
+ -fx-cursor: hand;
+}
+
+.leaderboard-row:hover {
+ -fx-background-color: rgba(255,255,255,0.03);
+}
+
+/* The Blue "My Rank" Footer */
+.my-rank-footer {
+ -fx-background-color: linear-gradient(to right, #463eef, #6d66f5);
+ -fx-background-radius: 16;
+ -fx-padding: 15 ;
+ -fx-effect: dropshadow(three-pass-box, rgba(70, 62, 239, 0.4), 20, 0, 0, 8);
+}
From 0b97d427e8f25181d3e5162cae3303fb5d57f916 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:53:37 +0200
Subject: [PATCH 10/26] Add animated background and improve HomeController UI
Introduces BackgroundHomeAnimation for animated 'X' and 'O' particles in the home screen background. Refactors HomeController to support the new background, updates FXML fields, improves error handling, and adds leaderboard UI update logic.
---
.../presentation/BackgroundHomeAnimation.java | 90 ++++++++++++++
.../presentation/HomeController.java | 114 +++++++++++-------
2 files changed, 161 insertions(+), 43 deletions(-)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
new file mode 100644
index 0000000..37d5e2c
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
@@ -0,0 +1,90 @@
+package com.boredxgames.tictactoeclient.presentation;
+
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+
+
+/**
+ *
+ * @author Hazem
+ */
+
+
+import java.util.Random;
+import javafx.animation.FadeTransition;
+import javafx.animation.ParallelTransition;
+import javafx.animation.RotateTransition;
+import javafx.animation.TranslateTransition;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Pane;
+import javafx.util.Duration;
+
+
+public abstract class BackgroundHomeAnimation{
+
+ private static final Random random = new Random();
+
+ /**
+ * Animates a container sliding up from the bottom and fading in.
+ * @param node The container (e.g., VBox) to animate.
+ */
+ public static void animateCardEntry(Node node) {
+ node.setOpacity(0);
+ node.setTranslateY(500);
+
+ TranslateTransition tt = new TranslateTransition(Duration.millis(1500), node);
+ tt.setToY(0);
+
+ FadeTransition ft = new FadeTransition(Duration.millis(1000), node);
+ ft.setToValue(1);
+
+ ParallelTransition pt = new ParallelTransition(tt, ft);
+ pt.play();
+ }
+
+
+ public static void startBackgroundAnimation(Pane backgroundPane, double screenWidth, double screenHeight) {
+ int particleCount = 50;
+
+ for (int i = 0; i < particleCount; i++) {
+ Label particle = new Label(random.nextBoolean() ? "X" : "O");
+ particle.getStyleClass().add("background-particle");
+
+ double size = 15 + random.nextInt(50);
+ particle.setStyle("-fx-font-size: " + size + "px; -fx-text-fill: rgba(79, 94, 247, " + (0.05 + random.nextDouble() * 0.2) + ");");
+
+ particle.setTranslateX(random.nextInt((int) screenWidth));
+ particle.setTranslateY(random.nextInt((int) screenHeight));
+
+ backgroundPane.getChildren().add(particle);
+ animateParticle(particle, screenWidth, screenHeight);
+ }
+ }
+
+ private static void animateParticle(Label particle, double width, double height) {
+ TranslateTransition move = new TranslateTransition(Duration.seconds(15 + random.nextInt(15)), particle);
+ double endX = particle.getTranslateX() + (random.nextInt(200) - 100); // Drift sideways
+ double endY = -100;
+
+ move.setToX(endX);
+ move.setToY(endY);
+ move.setCycleCount(1);
+
+ RotateTransition rotate = new RotateTransition(Duration.seconds(5 + random.nextInt(10)), particle);
+ rotate.setByAngle(360);
+ rotate.setCycleCount(RotateTransition.INDEFINITE);
+
+ move.setOnFinished(e -> {
+
+ particle.setTranslateY(height + random.nextInt(100));
+ particle.setTranslateX(random.nextInt((int) width));
+ animateParticle(particle, width, height);
+ });
+
+ move.play();
+ rotate.play();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index dc46c96..93823e8 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -20,71 +20,76 @@
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
public class HomeController implements Initializable {
- @FXML private BorderPane rootPane;
+ @FXML private StackPane rootStack;
+ @FXML private BorderPane mainContent;
+ @FXML private Pane backgroundPane;
@FXML private Label usernameLabel;
@FXML private Label scoreLabel;
@FXML private PlayersController playersViewController;
+@FXML private LeaderboardController leaderboardViewController;
+ private Alert currentIncomingRequestAlert;
@Override
public void initialize(URL url, ResourceBundle rb) {
+
MessageRouter.setHomeController(this);
AuthResponseEntity currentUser = ServerConnectionManager.getInstance().getPlayer();
- System.out.println("currrrrent : "+currentUser.getUserName());
if (currentUser != null) {
if(usernameLabel != null) usernameLabel.setText(currentUser.getUserName());
- if(scoreLabel != null) scoreLabel.setText("score " + currentUser.getScore() + " P");
- }
- Platform.runLater(() -> {
- if (rootPane.getScene() != null) {
- rootPane.getScene().setFill(javafx.scene.paint.Color.valueOf("#111022"));
+ if(scoreLabel != null) scoreLabel.setText("Score: " + currentUser.getScore());
}
- javafx.scene.Node headerBackground = rootPane.lookup(".tab-header-background");
- if (headerBackground != null) {
- headerBackground.setStyle("-fx-background-color: transparent;");
- }
-
- javafx.scene.Node viewport = rootPane.lookup(".viewport");
- if (viewport != null) {
- viewport.setStyle("-fx-background-color: transparent;");
- }
- });
+ Platform.runLater(() -> {
+ if (backgroundPane != null) {
+ double w = rootStack.getWidth() > 0 ? rootStack.getWidth() : 1280;
+ double h = rootStack.getHeight() > 0 ? rootStack.getHeight() : 800;
+ BackgroundHomeAnimation.startBackgroundAnimation(backgroundPane, w, h);
+ }
+ javafx.scene.Node viewport = rootStack.lookup(".viewport");
+ if (viewport != null) viewport.setStyle("-fx-background-color: rgba(0,0,0,0);");
+ });
+ GameService.getInstance().requestLeaderboard();
GameService.getInstance().requestAvailablePlayers();
-
-
+
+
}
- public void updatePlayersList(AvailablePlayersInfo info) {
+ public void updatePlayersList(AvailablePlayersInfo info) {
if (playersViewController != null) {
- playersViewController.updateList(info);
- }
+ playersViewController.updateList(info);
+ }
}
-
-public void handleServerGameResponse(GameResponseInfo info) {
+ public void handleServerGameResponse(GameResponseInfo info) {
GameService.getInstance().setWaiting(false);
if (info.isAccepted()) {
- System.out.println("Opponent Accepted Starting Game");
-
+ System.out.println("Opponent Accepted. Waiting for GAME_START event...");
} else {
- System.out.println("Opponent Declined.");
- Alert alert = new Alert(Alert.AlertType.INFORMATION);
- alert.setTitle("Challenge Declined");
- alert.setHeaderText(null);
- alert.setContentText("Player " + info.getResponderUserName()+ " declined your challenge.");
- alert.showAndWait();
-
- GameService.getInstance().requestAvailablePlayers();
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Challenge Declined");
+ alert.setHeaderText(null);
+ alert.setContentText("Player " + info.getResponderUserName()+ " declined your challenge.");
+ alert.showAndWait();
+
+ GameService.getInstance().requestAvailablePlayers();
+ });
}
}
public void showIncomingGameRequest(GameRequestInfo info) {
Platform.runLater(() -> {
+ if (currentIncomingRequestAlert != null && currentIncomingRequestAlert.isShowing()) {
+ currentIncomingRequestAlert.close();
+ }
+
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Incoming Challenge");
alert.setHeaderText("Game Request");
@@ -92,30 +97,53 @@ public void showIncomingGameRequest(GameRequestInfo info) {
ButtonType acceptBtn = new ButtonType("Accept");
ButtonType declineBtn = new ButtonType("Decline");
-
alert.getButtonTypes().setAll(acceptBtn, declineBtn);
+
+ this.currentIncomingRequestAlert = alert; // Store reference
alert.showAndWait().ifPresent(type -> {
+ this.currentIncomingRequestAlert = null;
boolean accepted = (type == acceptBtn);
GameService.getInstance().sendGameResponse(info, accepted);
});
});
}
-
+
+
+ public void dismissIncomingRequest() {
+ Platform.runLater(() -> {
+ if (currentIncomingRequestAlert != null && currentIncomingRequestAlert.isShowing()) {
+ currentIncomingRequestAlert.setResult(ButtonType.CANCEL);
+ currentIncomingRequestAlert.close();
+ currentIncomingRequestAlert = null;
+ }
+ });
+ }
+
public void showErrorAlert(String message) {
- Alert alert = new Alert(Alert.AlertType.ERROR);
- alert.setTitle("Error");
- alert.setHeaderText(null);
- alert.setContentText(message);
- alert.showAndWait();
+ GameService.getInstance().setWaiting(false); // Always unblock waiting state on error
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle("Error");
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ alert.showAndWait();
+
+ // Refresh list to ensure UI is consistent
+ GameService.getInstance().requestAvailablePlayers();
+ });
}
@FXML
private void onSignOut() {
ServerConnectionManager.getInstance().disconnect();
-
ServerConnectionManager.getInstance().setPlayer(null);
-
NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE);
}
+
+ public void updateLeaderboardUI(AvailablePlayersInfo info) {
+ if (leaderboardViewController != null) {
+ leaderboardViewController.updateLeaderboard(info);
+ }
+}
}
\ No newline at end of file
From e625effcb58dced17f628c2bb015e796d2d13058 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:53:49 +0200
Subject: [PATCH 11/26] Enhance leaderboard UI and fix player status label
Refactored LeaderboardController to display a dynamic, styled leaderboard with rank icons, player avatars, and a footer for the current user's rank. Also updated PlayersController to label pending players as 'Pending' instead of 'BUSY' and added a debug print statement for challenge actions.
---
.../presentation/LeaderboardController.java | 114 +++++++++++++++---
.../presentation/PlayersController.java | 4 +-
2 files changed, 101 insertions(+), 17 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
index 9883f11..05f8e6f 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
@@ -1,26 +1,110 @@
-/*
- * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
- * Click nbfs://nbhost/SystemFileSystem/Templates/javafx/FXMLController.java to edit this template
- */
package com.boredxgames.tictactoeclient.presentation;
+import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
+import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
+import com.boredxgames.tictactoeclient.domain.model.PlayerEntity;
+import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import java.net.URL;
import java.util.ResourceBundle;
+import java.util.Vector;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
import javafx.fxml.Initializable;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Circle;
-/**
- * FXML Controller class
- *
- * @author moham
- */
public class LeaderboardController implements Initializable {
- /**
- * Initializes the controller class.
- */
+ @FXML private VBox leaderboardContainer;
+
+ @FXML private HBox myRankBar;
+ @FXML private Label myRankLabel;
+ @FXML private Label myNameLabel;
+ @FXML private Label myScoreLabel;
+
@Override
public void initialize(URL url, ResourceBundle rb) {
- // TODO
- }
+ }
-}
+ public void updateLeaderboard(AvailablePlayersInfo info) {
+ Platform.runLater(() -> {
+ leaderboardContainer.getChildren().clear();
+
+ Vector allPlayers = new Vector<>();
+ if (info.getOnlinePlayers() != null) allPlayers.addAll(info.getOnlinePlayers());
+ if (info.getInGamePlayers() != null) allPlayers.addAll(info.getInGamePlayers());
+ if (info.getPendingPlayers() != null) allPlayers.addAll(info.getPendingPlayers());
+
+ allPlayers.sort((p1, p2) -> Integer.compare(p2.getScore(), p1.getScore()));
+
+ AuthResponseEntity me = ServerConnectionManager.getInstance().getPlayer();
+ boolean foundMe = false;
+
+ int rank = 1;
+ for (PlayerEntity p : allPlayers) {
+ if (me != null && p.getId().equals(me.getId())) {
+ foundMe = true;
+ if (myRankBar != null) updateMyFooter(rank, p);
+ }
+
+ leaderboardContainer.getChildren().add(createRow(p, rank));
+ rank++;
+ }
+
+ if (myRankBar != null) myRankBar.setVisible(foundMe);
+ });
+ }
+
+ private void updateMyFooter(int rank, PlayerEntity p) {
+ if (myRankLabel != null) myRankLabel.setText(String.valueOf(rank));
+ if (myNameLabel != null) myNameLabel.setText(p.getUsername());
+ if (myScoreLabel != null) myScoreLabel.setText(String.valueOf(p.getScore()));
+ }
+
+ private HBox createRow(PlayerEntity p, int rank) {
+ HBox row = new HBox(20);
+ row.setAlignment(Pos.CENTER_LEFT);
+ row.getStyleClass().add("leaderboard-row");
+
+ Label rankLabel = new Label();
+ rankLabel.setPrefWidth(60);
+
+ if (rank == 1) {
+ rankLabel.setText("🏆");
+ rankLabel.setStyle("-fx-text-fill: #FFD700; -fx-font-size: 20px;");
+ } else if (rank == 2) {
+ rankLabel.setText("🥈");
+ rankLabel.setStyle("-fx-text-fill: #C0C0C0; -fx-font-size: 20px;");
+ } else if (rank == 3) {
+ rankLabel.setText("🥉");
+ rankLabel.setStyle("-fx-text-fill: #CD7F32; -fx-font-size: 20px;");
+ } else {
+ rankLabel.setText(String.valueOf(rank));
+ rankLabel.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 14px; -fx-font-weight: bold;");
+ }
+
+ HBox nameBox = new HBox(10);
+ nameBox.setAlignment(Pos.CENTER_LEFT);
+ HBox.setHgrow(nameBox, Priority.ALWAYS);
+
+ Circle avatar = new Circle(18);
+ avatar.getStyleClass().add("avatar-profile");
+
+ Label nameLabel = new Label(p.getUsername());
+ nameLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 14px;");
+
+ nameBox.getChildren().addAll(avatar, nameLabel);
+
+ Label scoreLabel = new Label(p.getScore() + " PTS");
+ scoreLabel.setPrefWidth(100);
+ scoreLabel.setAlignment(Pos.CENTER_RIGHT);
+ scoreLabel.setStyle("-fx-text-fill: #00ff9d; -fx-font-family: 'Monospaced'; -fx-font-weight: bold;");
+
+ row.getChildren().addAll(rankLabel, nameBox, scoreLabel);
+ return row;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
index 04235ca..68caded 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
@@ -38,7 +38,7 @@ public void updateList(AvailablePlayersInfo info) {
if (info.getPendingPlayers() != null) {
for (PlayerEntity p : info.getPendingPlayers()) {
- playersContainer.getChildren().add(createPlayerCard(p, "BUSY", "busy", false));
+ playersContainer.getChildren().add(createPlayerCard(p, "Pending", "pending", false));
}
}
@@ -92,7 +92,7 @@ private HBox createPlayerCard(PlayerEntity p, String statusText, String type, bo
actionBtn.getStyleClass().add("btn-challenge");
actionBtn.setOnAction(e -> {
-
+ System.out.println("response tyepeeeeeeeeeeeeeeeeeee : "+GameService.getInstance().isWaiting());
if (GameService.getInstance().isWaiting()) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Please Wait");
From 292f713a38deb73abca4e75dbf3d5454eb6bcdfb Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Sun, 11 Jan 2026 23:54:21 +0200
Subject: [PATCH 12/26] Add leaderboard support and improve error handling
Introduces GET_LEADERBOARD action and requestLeaderboard method to support leaderboard retrieval. Refactors AuthResponseEntity to use consistent 'score' naming. Enhances MessageRouter with leaderboard UI updates and improved error handling for room and opponent issues.
---
.../navigation/NavigationManager.java | 2 +-
.../domain/model/AuthResponseEntity.java | 10 ++++----
.../domain/services/communication/Action.java | 4 +++-
.../services/communication/MessageRouter.java | 23 +++++++++++++++++--
.../domain/services/game/GameService.java | 6 ++++-
5 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
index 2f86ada..c29829e 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
@@ -22,7 +22,7 @@ public static Scene init() throws IOException {
current = new ScreenNavigationEntry(Screens.PRIMARY, null, null);
- scene = new Scene(initRoot(current.screen().getName()), 1600, 900);
+ scene = new Scene(initRoot(current.screen().getName()), 1200, 800);
return scene;
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java
index 61d6624..873947f 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java
@@ -11,12 +11,12 @@
public class AuthResponseEntity {
private String id ;
private String userName ;
- private int Score;
+ private int score;
public AuthResponseEntity(String id, String userName, int Score) {
this.id = id;
this.userName = userName;
- this.Score = Score;
+ this.score = Score;
}
public AuthResponseEntity() {
@@ -31,7 +31,7 @@ public void setUserName(String userName) {
}
public void setScore(int Score) {
- this.Score = Score;
+ this.score = Score;
}
public String getId() {
@@ -43,7 +43,7 @@ public String getUserName() {
}
public int getScore() {
- return Score;
+ return score;
}
public static AuthResponseEntity createAuthEntity(String id ,String username , int score)
@@ -53,7 +53,7 @@ public static AuthResponseEntity createAuthEntity(String id ,String username , i
@Override
public String toString() {
- return "AuthRequestEntity{" + "id=" + id + ", userName=" + userName + ", Score=" + Score + '}';
+ return "AuthRequestEntity{" + "id=" + id + ", userName=" + userName + ", Score=" + score + '}';
}
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
index fe68cc7..2d8bc2d 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
@@ -25,7 +25,9 @@ public enum Action {
ROOM_NOT_FOUND(150),
INVALID_OPPONENT(160),
NO_PENDING_REQUEST(170),
- GET_AVAILABLE_PLAYERS(180);
+ GET_AVAILABLE_PLAYERS(180),
+ GET_LEADERBOARD(190);
+
final int id;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index 99d4d8b..51a37d9 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -93,6 +93,9 @@ private void handleResponse(Message msg) {
AuthenticationController.showUserAlert("Username not found");
}
+ case REQUEST_GAME -> {
+ System.out.println("Server acknowledged Game Request.");
+ }
case GET_AVAILABLE_PLAYERS -> {
AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
if (homeController != null) {
@@ -105,6 +108,12 @@ private void handleResponse(Message msg) {
Platform.runLater(() -> homeController.handleServerGameResponse(info));
}
}
+ case GET_LEADERBOARD -> {
+ AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.updateLeaderboardUI(info));
+ }
+}
default -> {
System.out.println("Unknown Action: " + action);
@@ -179,10 +188,20 @@ private void handleError(Message msg) {
Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
}
}
+
case ROOM_NOT_FOUND -> {
- String errorMsg = "Game room no longer exists.";
- System.out.println(errorMsg);
+ String errorMsg = "The game session is no longer available.";
+ if (homeController != null) {
+ // Close the "Accept/Decline" popup if open
+ homeController.dismissIncomingRequest();
+ Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
+ }
+ }
+
+ case INVALID_OPPONENT -> {
+ String errorMsg = "Opponent disconnected.";
if (homeController != null) {
+ homeController.dismissIncomingRequest();
Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
}
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
index 87bc8b8..7fcbcbd 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
@@ -22,7 +22,7 @@ public class GameService {
private static GameService instance;
private final ServerConnectionManager connection;
- private boolean isWaitingForResponse = false;
+ private boolean isWaitingForResponse = false;
private GameService() {
this.connection = ServerConnectionManager.getInstance();
@@ -79,4 +79,8 @@ public void sendGameResponse(GameRequestInfo originalRequest, boolean accepted)
Message msg = Message.createMessage(MessageType.RESPONSE, Action.GAME_RESPONSE, responseInfo);
connection.sendMessage(msg);
}
+ public void requestLeaderboard() {
+ Message msg = Message.createMessage(MessageType.REQUEST, Action.GET_LEADERBOARD, "");
+ connection.sendMessage(msg);
+ }
}
\ No newline at end of file
From f13d1549af27268b01596f2cd79011554a6e8c5b Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Mon, 12 Jan 2026 00:11:49 +0200
Subject: [PATCH 13/26] disconnect from server on navigation back action
---
.../tictactoeclient/presentation/AuthenticationController.java | 1 +
.../presentation/Server_connectionController.java | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
index 61058f5..3b535c6 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
@@ -109,6 +109,7 @@ private String validateInput(String username, String password) {
@FXML
private void onBackClicked() {
+ ServerConnectionManager.getInstance().disconnect();
NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE);
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java
index d60953b..2361708 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java
@@ -103,6 +103,7 @@ private void onBackClicked() {
if (connectionThread != null && connectionThread.isAlive()) {
connectionThread.interrupt();
}
+ ServerConnectionManager.getInstance().disconnect();
NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE);
}
}
\ No newline at end of file
From 230f14d1431efd3d4a5cd33977b2f000be9d4604 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Mon, 12 Jan 2026 00:24:34 +0200
Subject: [PATCH 14/26] Fix enum and switch statement formatting issues
Corrected the Action enum to properly separate constants with commas and fixed indentation.
---
.../tictactoeclient/domain/services/communication/Action.java | 4 ++--
.../domain/services/communication/MessageRouter.java | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
index 6dbe79e..72fcdc2 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java
@@ -26,8 +26,8 @@ public enum Action {
INVALID_OPPONENT(160),
NO_PENDING_REQUEST(170),
GET_AVAILABLE_PLAYERS(180),
- USER_IS_ONLINE(190);
- GET_LEADERBOARD(200);
+ USER_IS_ONLINE(190),
+ GET_LEADERBOARD(200);
final int id;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index f83ba2b..a424b63 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -217,6 +217,7 @@ private void handleError(Message msg) {
homeController.dismissIncomingRequest();
Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
}
+ }
case INVALID_CREDENTIAL->{
System.out.println("INVALID_CREDENTIAL");
AuthenticationController.showUserAlert("INVALID CREDENTIAL");
From 84beb136e8ce3294bae42d3640a8435ae1fce81c Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Mon, 12 Jan 2026 17:22:33 +0200
Subject: [PATCH 15/26] Update GameModeScreenController.java
---
.../tictactoeclient/presentation/GameModeScreenController.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
index ca4c113..7ca0311 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
@@ -58,7 +58,7 @@ private void setupActions() {
onlineCard.setOnMouseClicked(e -> {
System.out.println("Online Mode Selected");
- //NavigationManager.navigate(Screens.ONLINE, NavigationAction.PUSH);
+ NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.PUSH);
});
}
From 7b01eaaf22fec44397deb3b372c797bd18d5eb13 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Mon, 12 Jan 2026 17:35:40 +0200
Subject: [PATCH 16/26] Add settings button action to home screen
Implemented the openSettings method in HomeController and connected it to the settings button in home.fxml. This allows navigation to the settings screen when the settings button is clicked.
---
.../tictactoeclient/presentation/HomeController.java | 11 ++++++++---
src/main/resources/fxml/home.fxml | 4 ++--
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index 93823e8..c87aad5 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -14,6 +14,7 @@
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
@@ -99,7 +100,7 @@ public void showIncomingGameRequest(GameRequestInfo info) {
ButtonType declineBtn = new ButtonType("Decline");
alert.getButtonTypes().setAll(acceptBtn, declineBtn);
- this.currentIncomingRequestAlert = alert; // Store reference
+ this.currentIncomingRequestAlert = alert;
alert.showAndWait().ifPresent(type -> {
this.currentIncomingRequestAlert = null;
@@ -121,7 +122,7 @@ public void dismissIncomingRequest() {
}
public void showErrorAlert(String message) {
- GameService.getInstance().setWaiting(false); // Always unblock waiting state on error
+ GameService.getInstance().setWaiting(false);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
@@ -129,7 +130,6 @@ public void showErrorAlert(String message) {
alert.setContentText(message);
alert.showAndWait();
- // Refresh list to ensure UI is consistent
GameService.getInstance().requestAvailablePlayers();
});
}
@@ -146,4 +146,9 @@ public void updateLeaderboardUI(AvailablePlayersInfo info) {
leaderboardViewController.updateLeaderboard(info);
}
}
+ @FXML
+ private void openSettings(ActionEvent event) {
+ System.out.println("Settings Button Clicked");
+ NavigationManager.navigate(Screens.SETTINGS, NavigationAction.PUSH);
+ }
}
\ No newline at end of file
diff --git a/src/main/resources/fxml/home.fxml b/src/main/resources/fxml/home.fxml
index 9bc7274..4f1fe7f 100644
--- a/src/main/resources/fxml/home.fxml
+++ b/src/main/resources/fxml/home.fxml
@@ -33,7 +33,7 @@
-
+
@@ -59,4 +59,4 @@
-
\ No newline at end of file
+
From 00e05a6bf61fe75b533758369ee60d1726dadea9 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Mon, 12 Jan 2026 18:07:35 +0200
Subject: [PATCH 17/26] Improve leaderboard ranking and display logic
Refactored updateLeaderboard to handle cases where the player's score is zero by displaying 'NA' as rank. Limited leaderboard display to top 5 players with positive scores and improved visibility logic for the current player's rank bar.
---
.../presentation/LeaderboardController.java | 38 ++++++++++++-------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
index 05f8e6f..db87a2d 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java
@@ -30,37 +30,49 @@ public class LeaderboardController implements Initializable {
public void initialize(URL url, ResourceBundle rb) {
}
- public void updateLeaderboard(AvailablePlayersInfo info) {
+ public void updateLeaderboard(AvailablePlayersInfo info) {
Platform.runLater(() -> {
leaderboardContainer.getChildren().clear();
-
+
Vector allPlayers = new Vector<>();
if (info.getOnlinePlayers() != null) allPlayers.addAll(info.getOnlinePlayers());
if (info.getInGamePlayers() != null) allPlayers.addAll(info.getInGamePlayers());
if (info.getPendingPlayers() != null) allPlayers.addAll(info.getPendingPlayers());
-
+
allPlayers.sort((p1, p2) -> Integer.compare(p2.getScore(), p1.getScore()));
AuthResponseEntity me = ServerConnectionManager.getInstance().getPlayer();
boolean foundMe = false;
- int rank = 1;
- for (PlayerEntity p : allPlayers) {
- if (me != null && p.getId().equals(me.getId())) {
+ if (me != null && myRankBar != null) {
+ PlayerEntity myPlayerState = allPlayers.stream()
+ .filter(p -> p.getId().equals(me.getId()))
+ .findFirst()
+ .orElse(null);
+
+ if (myPlayerState != null) {
foundMe = true;
- if (myRankBar != null) updateMyFooter(rank, p);
+ if (myPlayerState.getScore() == 0) {
+ updateMyFooter("NA", myPlayerState);
+ } else {
+ int myRank = allPlayers.indexOf(myPlayerState) + 1;
+ updateMyFooter(String.valueOf(myRank), myPlayerState);
+ }
}
+ myRankBar.setVisible(foundMe);
+ }
+ int count = 0;
+ for (PlayerEntity p : allPlayers) {
+ if (count >= 5 || p.getScore() <= 0) break;
- leaderboardContainer.getChildren().add(createRow(p, rank));
- rank++;
+ leaderboardContainer.getChildren().add(createRow(p, count + 1));
+ count++;
}
-
- if (myRankBar != null) myRankBar.setVisible(foundMe);
});
}
- private void updateMyFooter(int rank, PlayerEntity p) {
- if (myRankLabel != null) myRankLabel.setText(String.valueOf(rank));
+ private void updateMyFooter(String rankText, PlayerEntity p) {
+ if (myRankLabel != null) myRankLabel.setText(rankText);
if (myNameLabel != null) myNameLabel.setText(p.getUsername());
if (myScoreLabel != null) myScoreLabel.setText(String.valueOf(p.getScore()));
}
From 4e1a5de60d4700bcf108e6825c1371b3a045762d Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Tue, 13 Jan 2026 10:41:14 +0200
Subject: [PATCH 18/26] Rename GameService to OnlinGameSession
Renamed the GameService class to OnlinGameSession and updated all references in HomeController and PlayersController to use the new class name. This improves clarity and consistency in naming related to online game session management.
---
.../{GameService.java => OnlinGameSession.java} | 10 +++++-----
.../presentation/HomeController.java | 16 ++++++++--------
.../presentation/PlayersController.java | 8 ++++----
3 files changed, 17 insertions(+), 17 deletions(-)
rename src/main/java/com/boredxgames/tictactoeclient/domain/services/game/{GameService.java => OnlinGameSession.java} (92%)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java
similarity index 92%
rename from src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
rename to src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java
index 7fcbcbd..f483e61 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java
@@ -18,19 +18,19 @@
import com.boredxgames.tictactoeclient.domain.services.communication.Message;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
-public class GameService {
+public class OnlinGameSession {
- private static GameService instance;
+ private static OnlinGameSession instance;
private final ServerConnectionManager connection;
private boolean isWaitingForResponse = false;
- private GameService() {
+ private OnlinGameSession() {
this.connection = ServerConnectionManager.getInstance();
}
- public static synchronized GameService getInstance() {
+ public static synchronized OnlinGameSession getInstance() {
if (instance == null) {
- instance = new GameService();
+ instance = new OnlinGameSession();
}
return instance;
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index c87aad5..c514b67 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -9,7 +9,7 @@
import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter;
-import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+import com.boredxgames.tictactoeclient.domain.services.game.OnlinGameSession;
import java.net.URL;
import java.util.ResourceBundle;
@@ -55,8 +55,8 @@ public void initialize(URL url, ResourceBundle rb) {
javafx.scene.Node viewport = rootStack.lookup(".viewport");
if (viewport != null) viewport.setStyle("-fx-background-color: rgba(0,0,0,0);");
});
- GameService.getInstance().requestLeaderboard();
- GameService.getInstance().requestAvailablePlayers();
+ OnlinGameSession.getInstance().requestLeaderboard();
+ OnlinGameSession.getInstance().requestAvailablePlayers();
}
@@ -68,7 +68,7 @@ public void updatePlayersList(AvailablePlayersInfo info) {
}
public void handleServerGameResponse(GameResponseInfo info) {
- GameService.getInstance().setWaiting(false);
+ OnlinGameSession.getInstance().setWaiting(false);
if (info.isAccepted()) {
System.out.println("Opponent Accepted. Waiting for GAME_START event...");
@@ -80,7 +80,7 @@ public void handleServerGameResponse(GameResponseInfo info) {
alert.setContentText("Player " + info.getResponderUserName()+ " declined your challenge.");
alert.showAndWait();
- GameService.getInstance().requestAvailablePlayers();
+ OnlinGameSession.getInstance().requestAvailablePlayers();
});
}
}
@@ -105,7 +105,7 @@ public void showIncomingGameRequest(GameRequestInfo info) {
alert.showAndWait().ifPresent(type -> {
this.currentIncomingRequestAlert = null;
boolean accepted = (type == acceptBtn);
- GameService.getInstance().sendGameResponse(info, accepted);
+ OnlinGameSession.getInstance().sendGameResponse(info, accepted);
});
});
}
@@ -122,7 +122,7 @@ public void dismissIncomingRequest() {
}
public void showErrorAlert(String message) {
- GameService.getInstance().setWaiting(false);
+ OnlinGameSession.getInstance().setWaiting(false);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
@@ -130,7 +130,7 @@ public void showErrorAlert(String message) {
alert.setContentText(message);
alert.showAndWait();
- GameService.getInstance().requestAvailablePlayers();
+ OnlinGameSession.getInstance().requestAvailablePlayers();
});
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
index 68caded..a9dddc9 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java
@@ -2,7 +2,7 @@
import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
import com.boredxgames.tictactoeclient.domain.model.PlayerEntity;
-import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+import com.boredxgames.tictactoeclient.domain.services.game.OnlinGameSession;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
@@ -92,8 +92,8 @@ private HBox createPlayerCard(PlayerEntity p, String statusText, String type, bo
actionBtn.getStyleClass().add("btn-challenge");
actionBtn.setOnAction(e -> {
- System.out.println("response tyepeeeeeeeeeeeeeeeeeee : "+GameService.getInstance().isWaiting());
- if (GameService.getInstance().isWaiting()) {
+ System.out.println("response tyepeeeeeeeeeeeeeeeeeee : "+OnlinGameSession.getInstance().isWaiting());
+ if (OnlinGameSession.getInstance().isWaiting()) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Please Wait");
alert.setHeaderText(null);
@@ -104,7 +104,7 @@ private HBox createPlayerCard(PlayerEntity p, String statusText, String type, bo
actionBtn.setDisable(true);
actionBtn.setText("Sent...");
- GameService.getInstance().sendGameRequest(p.getId());
+ OnlinGameSession.getInstance().sendGameRequest(p.getId());
});
} else {
From bd8e8a5ce7c912e21560c499a73c2184a5bd45bd Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Tue, 13 Jan 2026 13:48:31 +0200
Subject: [PATCH 19/26] feat: implement offline mode selection screen and
navigation
---
.../com/boredxgames/tictactoeclient/App.java | 1 +
.../navigation/NavigationManager.java | 8 -
.../domain/managers/navigation/Screens.java | 3 +-
.../services/communication/MessageRouter.java | 1 -
.../domain/services/game/GameManager.java | 47 ----
.../services/game/OnlineGameManager.java | 77 -------
.../presentation/GameController.java | 2 +-
.../GameModeScreenController.java | 4 +-
.../OfflineModeSelectionController.java | 42 ++++
.../css/features/offline_mode_selection.css | 216 ++++++++++++++++++
src/main/resources/css/themes/green.css | 6 -
src/main/resources/css/themes/purple.css | 6 -
src/main/resources/css/themes/red.css | 6 -
.../fxml/offline_mode_selection.fxml | 210 +++++++++++++++++
14 files changed, 473 insertions(+), 156 deletions(-)
delete mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
delete mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
create mode 100644 src/main/resources/css/features/offline_mode_selection.css
delete mode 100644 src/main/resources/css/themes/green.css
delete mode 100644 src/main/resources/css/themes/purple.css
delete mode 100644 src/main/resources/css/themes/red.css
create mode 100644 src/main/resources/fxml/offline_mode_selection.fxml
diff --git a/src/main/java/com/boredxgames/tictactoeclient/App.java b/src/main/java/com/boredxgames/tictactoeclient/App.java
index b11731e..873223a 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/App.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/App.java
@@ -15,6 +15,7 @@ public class App extends Application {
@Override
public void start(Stage stage) throws IOException {
Scene scene = NavigationManager.init();
+ ThemeManager.init(scene);
stage.setScene(scene);
stage.show();
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
index d223dbd..29ff771 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java
@@ -20,17 +20,10 @@ private NavigationManager() {
private static final Stack screenStack = new Stack<>();
public static Scene init() throws IOException {
-// شاشة البداية
current = new ScreenNavigationEntry(Screens.PRIMARY, null, null);
scene = new Scene(initRoot(current.screen().getName()), 1200, 800);
- Parent root = initRoot(current.screen().getName());
- scene = new Scene(root, 640, 480);
-
- // طبق الثيم على الـ Scene الرئيسي وفعل listener لأي تغيير مستقبلي
- ThemeManager.init(scene);
-
return scene;
}
@@ -107,7 +100,6 @@ private static Parent initRoot(String fxml) throws IOException {
Parent root = loader.load();
- current = new ScreenNavigationEntry(Screens.PRIMARY, null, loader.getController());
return root;
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
index 32a9fc6..28b8b99 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
@@ -5,9 +5,10 @@ public enum Screens {
SECONDARY("GameModeScreen"),
SETTINGS("SettingsScreen"),
GAME_MODE("GameModeScreen"),
+ OFFLINE_MODE_SELECTION("offline_mode_selection"),
SERVER_CONNECTION("server_connection"),
AUTHENTICATION("authentication"),
- Home("home"),
+ Home("home"),
GAME("game_screen");
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index 224a03c..eef8071 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -24,7 +24,6 @@
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USERNAME_NOT_FOUND;
import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USER_IS_ONLINE;
-import com.boredxgames.tictactoeclient.domain.services.game.OnlineGameManager;
import com.boredxgames.tictactoeclient.presentation.AuthenticationController;
import com.boredxgames.tictactoeclient.presentation.HomeController;
import com.google.gson.Gson;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
deleted file mode 100644
index 861a3a9..0000000
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameManager.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.boredxgames.tictactoeclient.domain.services.game;
-
-import com.boredxgames.tictactoeclient.domain.services.GameBoard;
-import com.boredxgames.tictactoeclient.domain.services.GameBoard.GameState;
-
-public abstract class GameManager {
-
- protected GameBoard gameBoard;
- protected GameStateListener listener;
-
- public interface GameStateListener {
- void onBoardUpdated();
- void onGameOver(GameState state, char winner);
- void onError(String message);
- }
-
- public GameManager(GameStateListener listener) {
- this.listener = listener;
- this.gameBoard = new GameBoard();
- }
-
- public abstract void makeMove(int row, int col);
-
- public void resetGame() {
- gameBoard.resetGame();
- if (listener != null) {
- listener.onBoardUpdated();
- }
- }
-
- public GameBoard getGameBoard() {
- return gameBoard;
- }
-
- public char getCurrentPlayer() {
- return gameBoard.getCurrentPlayer();
- }
-
- protected void checkGameState() {
- if (listener == null) return;
-
- GameState state = gameBoard.getGameState();
- if (state != GameState.IN_PROGRESS) {
- listener.onGameOver(state, gameBoard.getWinner());
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
deleted file mode 100644
index 25bb2c8..0000000
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlineGameManager.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.boredxgames.tictactoeclient.domain.services.game;
-
-import com.boredxgames.tictactoeclient.domain.model.MoveInfo;
-
-import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
-
-import com.boredxgames.tictactoeclient.domain.services.communication.Action;
-
-import com.boredxgames.tictactoeclient.domain.services.communication.Message;
-
-import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
-
-
-public class OnlineGameManager extends GameManager {
-
- private final String roomId;
- private final char localPlayerSymbol;
- private final String playerId;
-
- public OnlineGameManager(GameStateListener listener, String roomId, char localPlayerSymbol, String playerId) {
- super(listener);
- this.roomId = roomId;
- this.localPlayerSymbol = localPlayerSymbol;
- this.playerId = playerId;
- }
-
- @Override
- public void makeMove(int row, int col) {
- if (gameBoard.getCurrentPlayer() != localPlayerSymbol) {
- if (listener != null) {
- listener.onError("It is not your turn!");
- }
- return;
- }
-
- if (!gameBoard.isValidMove(row, col)) {
- if (listener != null) listener.onError("Invalid Move");
- return;
- }
-
- int[] moveData = new int[]{row, col};
- MoveInfo moveInfo = new MoveInfo(playerId, moveData, roomId);
- Message message = Message.createMessage(MessageType.REQUEST, Action.SEND_GAME_UPDATE, moveInfo);
-
- ServerConnectionManager.getInstance().sendMessage(message);
-
- boolean success = gameBoard.makeMove(row, col, localPlayerSymbol);
- if (success) {
- if (listener != null) {
- listener.onBoardUpdated();
- checkGameState();
- }
- }
- }
-
- public void onNetworkMoveReceived(int row, int col, char playerSymbol) {
- if (gameBoard.getCellValue(row, col) == playerSymbol) {
- return;
- }
-
- boolean success = gameBoard.makeMove(row, col, playerSymbol);
-
- if (success) {
- if (listener != null) {
- listener.onBoardUpdated();
- checkGameState();
- }
- } else {
- if (listener != null) listener.onError("Game desynchronized.");
- }
- }
-
- @Override
- public void resetGame() {
- throw new UnsupportedOperationException("Can't reset an online game.");
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 457b824..8c8c136 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -154,7 +154,7 @@ private void applyGameModeSettings() {
difficultyBadge.setManaged(false);
changeDifficultyButton.setVisible(false);
changeDifficultyButton.setManaged(false);
- // gameService = new OnlinePVPService(); // TODO: implement online pvp service
+
}
}
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
index cc0863d..cc806ea 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java
@@ -50,10 +50,8 @@ public void initialize() {
private void setupActions() {
- // عند الضغط على الكارد الخاص بالـ Offline
offlineCard.setOnMouseClicked(e -> {
- System.out.println("Offline Mode Selected");
- // NavigationManager.navigate(Screens.OFFLINE, NavigationAction.PUSH);
+ NavigationManager.navigate(Screens.OFFLINE_MODE_SELECTION, NavigationAction.PUSH);
});
onlineCard.setOnMouseClicked(e -> {
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
new file mode 100644
index 0000000..6a2c593
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
@@ -0,0 +1,42 @@
+package com.boredxgames.tictactoeclient.presentation;
+
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
+import javafx.fxml.Initializable;
+import javafx.fxml.FXML;
+import javafx.scene.input.MouseEvent;
+import javafx.event.ActionEvent;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+
+public class OfflineModeSelectionController implements Initializable {
+ @FXML
+ @Override
+ public void initialize(URL url, ResourceBundle resourceBundle) {
+
+ }
+ @FXML
+ private void handleSettings(ActionEvent event) {
+ NavigationManager.navigate(Screens.SETTINGS, NavigationAction.PUSH);
+ }
+ @FXML
+ private void handleBackToMainMenu(ActionEvent event) {
+ NavigationManager.pop();
+ }
+
+ @FXML
+ private void handlePlayerVsPlayer(MouseEvent event) {
+ // TODO: Navigate to PvP game screen
+ System.out.println("Player vs Player mode selected");
+ }
+
+ @FXML
+ private void handlePlayerVsCPU(MouseEvent event) {
+ // TODO: Navigate to PvE game screen
+ System.out.println("Player vs CPU mode selected");
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/resources/css/features/offline_mode_selection.css b/src/main/resources/css/features/offline_mode_selection.css
new file mode 100644
index 0000000..3c601d3
--- /dev/null
+++ b/src/main/resources/css/features/offline_mode_selection.css
@@ -0,0 +1,216 @@
+/* css/features/offline_mode_selection.css */
+
+.root {
+ /* Variables inherited from global theme */
+ /* -bg-main, -bg-card, -primary, -primary-soft, -text-main */
+
+ /* UPDATED: Unified radius variable used for cropping */
+ -offline-card-radius: 24;
+ -icon-size: 48;
+
+ -text-badge: derive(-text-main, -50%);
+
+ -fx-background-color: -bg-main;
+ -fx-font-family: 'System';
+}
+
+/* Background Effects */
+.background-effects {
+ -fx-opacity: 0.15;
+}
+
+.blur-circle-top, .blur-circle-bottom {
+ -fx-background-color: -primary;
+ -fx-background-radius: 1000;
+ -fx-effect: dropshadow(gaussian, -primary, 120, 0.6, 0, 0);
+}
+
+/* ================== HEADER ================== */
+.header {
+ -fx-border-color: derive(-bg-card, 20%);
+ -fx-border-width: 0 0 1 0;
+}
+
+.logo-section {
+ -fx-cursor: hand;
+ -fx-opacity: 1.0;
+}
+.logo-section:hover { -fx-opacity: 0.8; }
+
+.logo-icon-wrapper {
+ -fx-background-color: -primary-soft;
+ -fx-background-radius: 12;
+ -fx-alignment: center;
+}
+
+.logo-icon-wrapper SVGPath {
+ -fx-fill: -primary;
+}
+
+.logo-text {
+ -fx-font-size: 18px;
+ -fx-font-weight: bold;
+ -fx-text-fill: -text-main;
+}
+
+/* Settings Button */
+.icon-button {
+ -fx-background-color: -bg-card;
+ -fx-background-radius: 12;
+ -fx-cursor: hand;
+ -fx-min-width: 40px;
+ -fx-min-height: 40px;
+ -fx-padding: 0;
+}
+.icon-button:hover {
+ -fx-border-color: -primary;
+ -fx-border-width: 1;
+}
+.icon-button SVGPath {
+ -fx-fill: -text-main;
+}
+
+/* Typography */
+.title {
+ -fx-font-size: 42px;
+ -fx-font-weight: 800;
+ -fx-text-fill: -text-main;
+ -fx-alignment: center;
+}
+.subtitle {
+ -fx-font-size: 15px;
+ -fx-text-fill: -text-muted;
+ -fx-font-weight: 500;
+}
+
+/* ================== CARDS ================== */
+
+.game-card {
+ /* 1. Use the unified variable so container matches content */
+ -fx-background-radius: -offline-card-radius;
+ -fx-border-radius: -offline-card-radius;
+ -fx-background-color: -bg-card;
+
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 20, 0, 0, 10);
+
+ -fx-border-color: transparent;
+ -fx-border-width: 2;
+}
+
+.game-card:hover {
+ -fx-border-color: -primary;
+ /* Maintain the same radius on hover to prevent shape shifting */
+ -fx-background-radius: -offline-card-radius;
+ -fx-border-radius: -offline-card-radius;
+ -fx-effect: dropshadow(three-pass-box, -primary-soft, 40, 0.2, 0, 0);
+}
+
+/* BACKGROUND IMAGES (Cropped via radius) */
+.card-bg-image-pvp {
+ -fx-background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBuw3YJ7m8TLhz3GNAjD8Scf5cQLafn2bp8Qn2o_Ll0YnRvc5Ab_GqZNGey-746vql56GF4b6Sb0UKlABmsHyFQGa2mOfoV1nhTqEWHwqIn3ZPTFy1766m5UtDZBWmPT7H5a4_W3sL8oBT6ZJ74opM1_UsXjR4LR-Hs8VSTtUWD1axZbvQOF6UgXhNW53aKKwV-BU0E91jRE_LkaM9uukR2NIMq35DC3CE2ggLEMvTft7WS62_B5fMAyJ1oTzUDC7Vk-CL4LrZv30EV");
+ -fx-background-size: cover;
+ -fx-background-position: center;
+ /* 2. Radius matches parent to create crop effect */
+ -fx-background-radius: -offline-card-radius;
+ /* 3. Insets 0 ensures image fills the region exactly to the edge */
+ -fx-background-insets: 0;
+}
+
+.card-bg-image-pve {
+ -fx-background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuD_55qHepZ8p-aFLKxAwRFDeAfTWN8qIelTmbf9QTc6mt5b7JBTox0lLVDujXJT3iQbXoW9F2NKgKgrykD-m9tahZevwoRma9_Ul3pm0Te6Suzje9RtNlAIsJw4o3tIupJ5NfLPGqoFFAs0sg7icvB2sTi4Zdu_4tZAVuCUq6fVujXB6tIYLLGAybHRwF3Y0mPCdWtNYj_TCtu_uOgXabYd_CeW7FP5Uc-ratqZX7ESNF0H6lpNtF91FYYWHaHW9YWDSgPBKw8T2C0t");
+ -fx-background-size: cover;
+ -fx-background-position: center;
+ -fx-background-radius: -offline-card-radius;
+ -fx-background-insets: 0;
+}
+
+/* GRADIENT OVERLAY */
+.card-gradient {
+ -fx-background-radius: -offline-card-radius;
+ -fx-background-insets: 0;
+ -fx-background-color: linear-gradient(
+ to top,
+ -bg-main 0%,
+ derive(-bg-main, 20%) 40%,
+ derive(-bg-main, 80%) 100%
+ );
+ -fx-opacity: 0.9;
+}
+
+/* ================== CARD ICONS ================== */
+
+.card-icon-bg {
+ -fx-background-color: -primary-soft;
+ -fx-background-radius: 100;
+ -fx-alignment: center;
+
+ -fx-min-width: -icon-size;
+ -fx-min-height: -icon-size;
+ -fx-max-width: -icon-size;
+ -fx-max-height: -icon-size;
+}
+
+.card-icon-bg SVGPath {
+ -fx-fill: white;
+}
+
+.game-card:hover .card-icon-bg {
+ -fx-effect: dropshadow(gaussian, -primary, 10, 0.4, 0, 0);
+}
+
+/* ================== CARD TEXT & FOOTER ================== */
+
+.card-title {
+ -fx-font-size: 26px;
+ -fx-font-weight: bold;
+ -fx-text-fill: -text-main;
+}
+.game-card:hover .card-title {
+ -fx-text-fill: -primary;
+}
+
+.card-description {
+ -fx-font-size: 14px;
+ -fx-text-fill: -text-muted;
+ -fx-font-weight: 500;
+}
+
+.card-footer {
+ -fx-border-color: derive(-bg-card, 30%);
+ -fx-border-width: 1 0 0 0;
+}
+
+.card-badge {
+ -fx-font-size: 11px;
+ -fx-font-weight: bold;
+ -fx-text-fill: -text-badge;
+ -fx-opacity: 0.7;
+}
+
+.action-text {
+ -fx-font-size: 14px;
+ -fx-font-weight: bold;
+ -fx-text-fill: -primary;
+}
+.card-action SVGPath {
+ -fx-fill: -primary;
+}
+
+/* Back Button */
+.back-button {
+ -fx-background-color: transparent;
+ -fx-padding: 10 20 10 20;
+ -fx-background-radius: 12;
+ -fx-cursor: hand;
+}
+.back-button:hover {
+ -fx-background-color: -bg-card;
+}
+.back-button .back-text, .back-button SVGPath {
+ -fx-text-fill: -text-muted;
+ -fx-fill: -text-muted;
+ -fx-font-weight: bold;
+}
+.back-button:hover .back-text { -fx-text-fill: -primary; }
+.back-button:hover SVGPath { -fx-fill: -primary; }
\ No newline at end of file
diff --git a/src/main/resources/css/themes/green.css b/src/main/resources/css/themes/green.css
deleted file mode 100644
index 66c7571..0000000
--- a/src/main/resources/css/themes/green.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.root {
- -color-bg: #0f2a1a;
- -color-bg-alt: #163a25;
- -color-text: #ffffff;
- -color-accent: #4caf50;
-}
diff --git a/src/main/resources/css/themes/purple.css b/src/main/resources/css/themes/purple.css
deleted file mode 100644
index c12155a..0000000
--- a/src/main/resources/css/themes/purple.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.root {
- -color-bg: #2b1e3a;
- -color-bg-alt: #3a2752;
- -color-text: #ffffff;
- -color-accent: #b388ff;
-}
diff --git a/src/main/resources/css/themes/red.css b/src/main/resources/css/themes/red.css
deleted file mode 100644
index 2e18dba..0000000
--- a/src/main/resources/css/themes/red.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.root {
- -color-bg: #2a0f0f;
- -color-bg-alt: #3a1515;
- -color-text: #ffffff;
- -color-accent: #ff5c5c;
-}
diff --git a/src/main/resources/fxml/offline_mode_selection.fxml b/src/main/resources/fxml/offline_mode_selection.fxml
new file mode 100644
index 0000000..4301f1e
--- /dev/null
+++ b/src/main/resources/fxml/offline_mode_selection.fxml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From c4c0f0a88fad6cf9dc160ee740bfb10552aef953 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Tue, 13 Jan 2026 15:06:22 +0200
Subject: [PATCH 20/26] Remove Player model class
Deleted the Player.java file from the domain model. This may indicate a refactor or removal of unused code related to player representation.
---
.../tictactoeclient/domain/model/Player.java | 32 -------------------
1 file changed, 32 deletions(-)
delete mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java
deleted file mode 100644
index 6a634ad..0000000
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.boredxgames.tictactoeclient.domain.model;
-
-/**
- * @author Tasneem
- */
-public class Player {
- private String id;
- private String username;
- private int score;
-
- public Player(String id, String username, int score) {
- this.id = id;
- this.username = username;
- this.score = score;
- }
-
- public String getUsername() {
- return username;
- }
-
- public void setUsername(String username) {
- this.username = username;
- }
-
- public int getScore() {
- return score;
- }
-
- public void setScore(int score) {
- this.score = score;
- }
-}
From 2eaf86a61dfbf2a139c5db515506fff08929e5a7 Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Tue, 13 Jan 2026 17:36:59 +0200
Subject: [PATCH 21/26] feat: implement offline mode selection screen and
navigation
---
.../domain/model/MoveInfo.java | 25 +++-
.../domain/model/OnlineGameState.java | 5 +
.../network/ServerConnectionManager.java | 1 +
.../domain/services/GameService.java | 3 +-
.../services/communication/MessageRouter.java | 110 +++++++++---------
.../services/game/OnlinePVPService.java | 68 +++++++++++
.../presentation/GameController.java | 26 +++--
.../presentation/HomeController.java | 5 +-
.../css/features/offline_mode_selection.css | 23 ----
9 files changed, 170 insertions(+), 96 deletions(-)
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java
create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
index 9003782..88cc6c3 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java
@@ -4,6 +4,12 @@
*/
package com.boredxgames.tictactoeclient.domain.model;
+import com.boredxgames.tictactoeclient.domain.services.communication.Action;
+import com.boredxgames.tictactoeclient.domain.services.communication.Header;
+import com.boredxgames.tictactoeclient.domain.services.communication.Message;
+import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
+import com.google.gson.Gson;
+
/**
*
* @author mahmoud
@@ -11,9 +17,9 @@
public class MoveInfo {
private final String roomId;
private final String playerId;
- private final Object move;
+ private final String move;
- public MoveInfo(String playerId, Object move, String roomId) {
+ public MoveInfo(String playerId, String move, String roomId) {
this.playerId = playerId;
this.move = move;
this.roomId = roomId;
@@ -27,8 +33,19 @@ public String getPlayerId() {
return playerId;
}
- public Object getMove() {
+ public String getMove() {
return move;
}
-
+
+ public static MoveInfo createMoveInfo(String roomId, String playerId , Object data)
+ {
+ return new MoveInfo(playerId , toJson(data), roomId);
+ }
+
+ static private String toJson(Object data)
+ {
+ Gson gson = new Gson();
+ return gson.toJson(data);
+
+ }
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java
new file mode 100644
index 0000000..92229e9
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java
@@ -0,0 +1,5 @@
+package com.boredxgames.tictactoeclient.domain.model;
+
+public class OnlineGameState {
+ public static GameStartInfo info = null;
+}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
index 786480d..8f06f41 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java
@@ -98,6 +98,7 @@ public void disconnect() {
public synchronized void sendMessage(Message msg) {
try {
String jsonMessage = gson.toJson(msg);
+ System.out.println(jsonMessage + "SENTTTTT");
dos.writeUTF(jsonMessage);
dos.flush();
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
index 115c3de..4028f18 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java
@@ -1,7 +1,8 @@
-package com.boredxgames.tictactoeclient.domain.services.game;
+package com.boredxgames.tictactoeclient.domain.services;
import com.boredxgames.tictactoeclient.domain.model.GameState;
import com.boredxgames.tictactoeclient.domain.model.Move;
+import com.boredxgames.tictactoeclient.domain.services.game.GameBoard;
/**
* @author Tasneem
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index eef8071..827bbc0 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -8,22 +8,13 @@
*
* @author Hazem
*/
+
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
-import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
-import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
-import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo;
-import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
-import com.boredxgames.tictactoeclient.domain.model.GameStartInfo;
+import com.boredxgames.tictactoeclient.domain.model.*;
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.INTERNAL_SERVER_ERROR;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.INVALID_CREDENTIAL;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.LOGIN_SUCCESS;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.REGISTERATION_SUCCESS;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USERNAME_NOT_FOUND;
-import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USER_IS_ONLINE;
-
+import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService;
import com.boredxgames.tictactoeclient.presentation.AuthenticationController;
import com.boredxgames.tictactoeclient.presentation.HomeController;
import com.google.gson.Gson;
@@ -35,6 +26,7 @@ public class MessageRouter {
private static ServerConnectionManager connection;
private Gson gson = new Gson();
private static HomeController homeController;
+
private MessageRouter() {
connection = ServerConnectionManager.getInstance();
}
@@ -58,7 +50,8 @@ public void navigateMessage(String response) {
case ERROR -> handleError(message);
default -> System.out.println("Unknown MessageType: " + messageType);
}
- }
+ }
+
public static void setHomeController(HomeController controller) {
homeController = controller;
}
@@ -77,41 +70,40 @@ private Message handleRequest(Message msg) {
};
}
-
+
private void handleResponse(Message msg) {
Action action = msg.getHeader().getAction();
- System.out.println(action);
-
+ System.out.println(action);
+
switch (action) {
- case LOGIN_SUCCESS ->
- {
+ case LOGIN_SUCCESS -> {
System.out.println("Login Success");
- AuthResponseEntity responseData = gson.fromJson(msg.getData(),AuthResponseEntity.class);
- ServerConnectionManager.getInstance().setPlayer(responseData);
+ AuthResponseEntity responseData = gson.fromJson(msg.getData(), AuthResponseEntity.class);
+ ServerConnectionManager.getInstance().setPlayer(responseData);
System.out.println(responseData);
NavigationManager.navigate(Screens.Home, NavigationAction.REPLACE);
}
- case REGISTERATION_SUCCESS->{
+ case REGISTERATION_SUCCESS -> {
System.out.println("Registration success");
- AuthResponseEntity responseData = gson.fromJson(msg.getData(),AuthResponseEntity.class);
- AuthenticationController.showUserAlert("Registration success");
+ AuthResponseEntity responseData = gson.fromJson(msg.getData(), AuthResponseEntity.class);
+ AuthenticationController.showUserAlert("Registration success");
}
- case USERNAME_NOT_FOUND->{
+ case USERNAME_NOT_FOUND -> {
System.out.println("Username not found");
AuthenticationController.showUserAlert("Username not found");
-
+
}
case REQUEST_GAME -> {
- System.out.println("Server acknowledged Game Request.");
- }
+ System.out.println("Server acknowledged Game Request.");
+ }
case GET_AVAILABLE_PLAYERS -> {
AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
- if (homeController != null) {
- Platform.runLater(() -> homeController.updatePlayersList(info));
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.updatePlayersList(info));
+ }
}
- }
case GAME_RESPONSE -> {
GameResponseInfo info = gson.fromJson(msg.getData(), GameResponseInfo.class);
if (homeController != null) {
@@ -119,25 +111,30 @@ private void handleResponse(Message msg) {
}
}
case GET_LEADERBOARD -> {
- AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
- if (homeController != null) {
- Platform.runLater(() -> homeController.updateLeaderboardUI(info));
- }
-}
+ AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class);
+ if (homeController != null) {
+ Platform.runLater(() -> homeController.updateLeaderboardUI(info));
+ }
+ }
+ case SEND_GAME_UPDATE -> {
+ MoveInfo moveInfo = gson.fromJson(msg.getData(), MoveInfo.class);
+ OnlinePVPService.onIncomingMove(moveInfo);
+ }
default -> {
System.out.println("Unknown Action: " + action);
}
- };
+ }
+ ;
}
private void handleEvent(Message msg) {
Action action = msg.getHeader().getAction();
- System.out.println(action);
+ System.out.println(action);
switch (action) {
- case REQUEST_GAME -> {
+ case REQUEST_GAME -> {
GameRequestInfo info = gson.fromJson(msg.getData(), GameRequestInfo.class);
if (homeController != null) {
Platform.runLater(() -> homeController.showIncomingGameRequest(info));
@@ -145,8 +142,8 @@ private void handleEvent(Message msg) {
}
case GAME_START -> {
- GameStartInfo info = gson.fromJson(msg.getData(), GameStartInfo.class);
- NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE);
+ OnlineGameState.info = gson.fromJson(msg.getData(), GameStartInfo.class);
+ NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, GameMode.ONLINE_PVP);
}
@@ -154,25 +151,26 @@ private void handleEvent(Message msg) {
System.out.println("Unknown Action: " + action);
}
- };
+ }
+ ;
}
-
+
private void handleError(Message msg) {
System.out.println("my time has come");
Action action = msg.getHeader().getAction();
- System.out.println(action);
-
+ System.out.println(action);
+
switch (action) {
-
- case USERNAME_NOT_FOUND->{
+
+ case USERNAME_NOT_FOUND -> {
System.out.println("Username not found");
AuthenticationController.showUserAlert("Username not found");
-
+
}
- case INTERNAL_SERVER_ERROR->{
- String errorMessage = "Server error. Please try again.";
- System.out.println("Internal Server Error");
+ case INTERNAL_SERVER_ERROR -> {
+ String errorMessage = "Server error. Please try again.";
+ System.out.println("Internal Server Error");
AuthenticationController.showUserAlert("Internal Server Error");
if (homeController != null) {
Platform.runLater(() -> homeController.showErrorAlert(errorMessage));
@@ -217,18 +215,18 @@ private void handleError(Message msg) {
Platform.runLater(() -> homeController.showErrorAlert(errorMsg));
}
}
- case INVALID_CREDENTIAL->{
- System.out.println("INVALID_CREDENTIAL");
+ case INVALID_CREDENTIAL -> {
+ System.out.println("INVALID_CREDENTIAL");
AuthenticationController.showUserAlert("INVALID CREDENTIAL");
}
- case USER_IS_ONLINE->{
- System.out.println("User alread logged in");
+ case USER_IS_ONLINE -> {
+ System.out.println("User alread logged in");
AuthenticationController.showUserAlert("User alread logged in");
}
- case USERNAME_ALREADY_EXIST->{
- System.out.println("USERNAME_ALREADY_EXIST");
+ case USERNAME_ALREADY_EXIST -> {
+ System.out.println("USERNAME_ALREADY_EXIST");
AuthenticationController.showUserAlert("USERNAME ALREADY EXIST");
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
new file mode 100644
index 0000000..e1b3b75
--- /dev/null
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
@@ -0,0 +1,68 @@
+package com.boredxgames.tictactoeclient.domain.services.game;
+
+import com.boredxgames.tictactoeclient.domain.model.*;
+import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
+import com.boredxgames.tictactoeclient.domain.services.communication.Action;
+import com.boredxgames.tictactoeclient.domain.services.communication.Message;
+import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
+import com.google.gson.Gson;
+
+import java.util.function.Consumer;
+
+public class OnlinePVPService implements GameService {
+ private static OnlinePVPService instance;
+ public static OnlinePVPService getInstance() {
+ if (instance == null) {
+ instance = new OnlinePVPService();
+ }
+ return instance;
+ }
+ private OnlinePVPService(){}
+
+ // specific listener to bridge Network -> UI
+ private static Consumer moveListener;
+
+ public OnlinePVPService setMoveListener(Consumer listener) {
+ moveListener = listener;
+ return this;
+ }
+
+ public static void onIncomingMove(MoveInfo moveInfo) {
+ Gson gson = new Gson();
+ Move move = gson.fromJson(moveInfo.getMove(), Move.class);
+ if (moveListener != null) {
+ moveListener.accept(move);
+ }
+ }
+
+ @Override
+ public void makeMove(Move move, char currentPlayer) {
+
+ ServerConnectionManager connectionManager = ServerConnectionManager.getInstance();
+ AuthResponseEntity player = connectionManager.getPlayer();
+ GameStartInfo sessionInfo = OnlineGameState.info;
+ // Prepare the data
+ MoveInfo info = MoveInfo.createMoveInfo(sessionInfo.getRoomId() , player.getId(), move);
+
+ // Send SEND_GAME_UPDATE action to server
+ Message msg = Message.createMessage(
+ MessageType.RESPONSE,
+ Action.SEND_GAME_UPDATE,
+ info
+ );
+ connectionManager.sendMessage(msg);
+ }
+
+ @Override
+ public Move getNextMove(GameBoard board, char currentPlayer) {
+ // In online play, we don't calculate the next move locally.
+ // We wait for the server event instead.
+ return null;
+ }
+
+ @Override
+ public GameState getOutcome(GameBoard board) {
+ return board.getGameState();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 8c8c136..159c9a5 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -1,15 +1,16 @@
package com.boredxgames.tictactoeclient.presentation;
-import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationParameterAware;
-import com.boredxgames.tictactoeclient.domain.model.GameMode;
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
+import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationParameterAware;
import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
+import com.boredxgames.tictactoeclient.domain.model.GameMode;
import com.boredxgames.tictactoeclient.domain.model.GameNavigationParams;
+import com.boredxgames.tictactoeclient.domain.model.GameState;
import com.boredxgames.tictactoeclient.domain.model.Move;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
import com.boredxgames.tictactoeclient.domain.services.game.GameBoard;
-import com.boredxgames.tictactoeclient.domain.model.GameState;
-import com.boredxgames.tictactoeclient.domain.services.game.GameService;
+import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
@@ -27,6 +28,7 @@
import javafx.scene.media.MediaView;
import javafx.scene.text.Text;
import javafx.util.Duration;
+
import java.net.URL;
import java.util.Objects;
import java.util.ResourceBundle;
@@ -154,7 +156,14 @@ private void applyGameModeSettings() {
difficultyBadge.setManaged(false);
changeDifficultyButton.setVisible(false);
changeDifficultyButton.setManaged(false);
-
+ gameService = OnlinePVPService.getInstance().setMoveListener((move) ->
+ {
+ Platform.runLater(() -> {
+ updateCell(move.getCol(), move.getRow(), gameBoard.getCurrentPlayer());
+ gameBoard.switchPlayer();
+ enableBoard();
+ });
+ });
}
}
}
@@ -194,15 +203,16 @@ private void setupButtonHandlers() {
private void handleCellClick(int row, int col) {
if (!gameBoard.isValidMove(row, col)) return;
- if (gameMode == GameMode.ONLINE_PVP && !isPlayerTurn) return;
-
char currentPlayer = gameBoard.getCurrentPlayer();
Move move = new Move(row, col);
gameService.makeMove(move, currentPlayer);
+ updateCell(move.getCol(), move.getRow(), currentPlayer);
+ disableBoard();
+ gameBoard.switchPlayer();
Move nextMove = gameService.getNextMove(gameBoard, currentPlayer);
- if(nextMove != null) {
+ if (nextMove != null) {
Platform.runLater(() -> {
if (gameBoard.makeMove(nextMove.getRow(), nextMove.getCol(), gameBoard.getCurrentPlayer())) {
updateCell(nextMove.getRow(), nextMove.getCol(), gameBoard.getCurrentPlayer());
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index 5a83104..669a6b2 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -3,10 +3,7 @@
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction;
import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager;
import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens;
-import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity;
-import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo;
-import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo;
-import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo;
+import com.boredxgames.tictactoeclient.domain.model.*;
import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter;
import com.boredxgames.tictactoeclient.domain.services.game.OnlinGameSession;
diff --git a/src/main/resources/css/features/offline_mode_selection.css b/src/main/resources/css/features/offline_mode_selection.css
index 3c601d3..870c3ba 100644
--- a/src/main/resources/css/features/offline_mode_selection.css
+++ b/src/main/resources/css/features/offline_mode_selection.css
@@ -1,10 +1,4 @@
-/* css/features/offline_mode_selection.css */
-
.root {
- /* Variables inherited from global theme */
- /* -bg-main, -bg-card, -primary, -primary-soft, -text-main */
-
- /* UPDATED: Unified radius variable used for cropping */
-offline-card-radius: 24;
-icon-size: 48;
@@ -14,7 +8,6 @@
-fx-font-family: 'System';
}
-/* Background Effects */
.background-effects {
-fx-opacity: 0.15;
}
@@ -25,7 +18,6 @@
-fx-effect: dropshadow(gaussian, -primary, 120, 0.6, 0, 0);
}
-/* ================== HEADER ================== */
.header {
-fx-border-color: derive(-bg-card, 20%);
-fx-border-width: 0 0 1 0;
@@ -53,7 +45,6 @@
-fx-text-fill: -text-main;
}
-/* Settings Button */
.icon-button {
-fx-background-color: -bg-card;
-fx-background-radius: 12;
@@ -70,7 +61,6 @@
-fx-fill: -text-main;
}
-/* Typography */
.title {
-fx-font-size: 42px;
-fx-font-weight: 800;
@@ -83,10 +73,7 @@
-fx-font-weight: 500;
}
-/* ================== CARDS ================== */
-
.game-card {
- /* 1. Use the unified variable so container matches content */
-fx-background-radius: -offline-card-radius;
-fx-border-radius: -offline-card-radius;
-fx-background-color: -bg-card;
@@ -100,20 +87,16 @@
.game-card:hover {
-fx-border-color: -primary;
- /* Maintain the same radius on hover to prevent shape shifting */
-fx-background-radius: -offline-card-radius;
-fx-border-radius: -offline-card-radius;
-fx-effect: dropshadow(three-pass-box, -primary-soft, 40, 0.2, 0, 0);
}
-/* BACKGROUND IMAGES (Cropped via radius) */
.card-bg-image-pvp {
-fx-background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBuw3YJ7m8TLhz3GNAjD8Scf5cQLafn2bp8Qn2o_Ll0YnRvc5Ab_GqZNGey-746vql56GF4b6Sb0UKlABmsHyFQGa2mOfoV1nhTqEWHwqIn3ZPTFy1766m5UtDZBWmPT7H5a4_W3sL8oBT6ZJ74opM1_UsXjR4LR-Hs8VSTtUWD1axZbvQOF6UgXhNW53aKKwV-BU0E91jRE_LkaM9uukR2NIMq35DC3CE2ggLEMvTft7WS62_B5fMAyJ1oTzUDC7Vk-CL4LrZv30EV");
-fx-background-size: cover;
-fx-background-position: center;
- /* 2. Radius matches parent to create crop effect */
-fx-background-radius: -offline-card-radius;
- /* 3. Insets 0 ensures image fills the region exactly to the edge */
-fx-background-insets: 0;
}
@@ -125,7 +108,6 @@
-fx-background-insets: 0;
}
-/* GRADIENT OVERLAY */
.card-gradient {
-fx-background-radius: -offline-card-radius;
-fx-background-insets: 0;
@@ -138,8 +120,6 @@
-fx-opacity: 0.9;
}
-/* ================== CARD ICONS ================== */
-
.card-icon-bg {
-fx-background-color: -primary-soft;
-fx-background-radius: 100;
@@ -159,8 +139,6 @@
-fx-effect: dropshadow(gaussian, -primary, 10, 0.4, 0, 0);
}
-/* ================== CARD TEXT & FOOTER ================== */
-
.card-title {
-fx-font-size: 26px;
-fx-font-weight: bold;
@@ -197,7 +175,6 @@
-fx-fill: -primary;
}
-/* Back Button */
.back-button {
-fx-background-color: transparent;
-fx-padding: 10 20 10 20;
From cbfc526cc02d0dcf5507aaede498e1a98b450553 Mon Sep 17 00:00:00 2001
From: "hazemkora660@gmail.com"
Date: Tue, 13 Jan 2026 19:19:39 +0200
Subject: [PATCH 22/26] add which player turn
---
.../services/game/OnlinePVPService.java | 34 ++++++-
.../presentation/BackgroundHomeAnimation.java | 99 +++++++++----------
.../presentation/GameController.java | 70 +++++++++----
3 files changed, 127 insertions(+), 76 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
index e1b3b75..30e143a 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
@@ -7,18 +7,44 @@
import com.boredxgames.tictactoeclient.domain.services.communication.Message;
import com.boredxgames.tictactoeclient.domain.services.communication.MessageType;
import com.google.gson.Gson;
-
import java.util.function.Consumer;
public class OnlinePVPService implements GameService {
+
private static OnlinePVPService instance;
+ private boolean isMyTurn = false;
+
+ public void setupOnlineGame() {
+
+ System.out.println(" Check equality"+OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId()));
+
+
+ if (OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId())) {
+ isMyTurn = true;
+ }
+ }
+
+ public void setIsMyTurn(boolean isMyTurn) {
+ this.isMyTurn = isMyTurn;
+ }
+
+ public boolean isIsMyTurn() {
+ return isMyTurn;
+ }
+
+ public boolean checkTurn() {
+ return isMyTurn;
+ }
+
public static OnlinePVPService getInstance() {
if (instance == null) {
instance = new OnlinePVPService();
}
return instance;
}
- private OnlinePVPService(){}
+
+ private OnlinePVPService() {
+ }
// specific listener to bridge Network -> UI
private static Consumer moveListener;
@@ -43,7 +69,7 @@ public void makeMove(Move move, char currentPlayer) {
AuthResponseEntity player = connectionManager.getPlayer();
GameStartInfo sessionInfo = OnlineGameState.info;
// Prepare the data
- MoveInfo info = MoveInfo.createMoveInfo(sessionInfo.getRoomId() , player.getId(), move);
+ MoveInfo info = MoveInfo.createMoveInfo(sessionInfo.getRoomId(), player.getId(), move);
// Send SEND_GAME_UPDATE action to server
Message msg = Message.createMessage(
@@ -65,4 +91,4 @@ public Move getNextMove(GameBoard board, char currentPlayer) {
public GameState getOutcome(GameBoard board) {
return board.getGameState();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
index 37d5e2c..1b86222 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java
@@ -13,14 +13,9 @@
import java.util.Random;
-import javafx.animation.FadeTransition;
-import javafx.animation.ParallelTransition;
-import javafx.animation.RotateTransition;
-import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
-import javafx.util.Duration;
public abstract class BackgroundHomeAnimation{
@@ -32,59 +27,59 @@ public abstract class BackgroundHomeAnimation{
* @param node The container (e.g., VBox) to animate.
*/
public static void animateCardEntry(Node node) {
- node.setOpacity(0);
- node.setTranslateY(500);
-
- TranslateTransition tt = new TranslateTransition(Duration.millis(1500), node);
- tt.setToY(0);
-
- FadeTransition ft = new FadeTransition(Duration.millis(1000), node);
- ft.setToValue(1);
-
- ParallelTransition pt = new ParallelTransition(tt, ft);
- pt.play();
+// node.setOpacity(0);
+// node.setTranslateY(500);
+//
+// TranslateTransition tt = new TranslateTransition(Duration.millis(1500), node);
+// tt.setToY(0);
+//
+// FadeTransition ft = new FadeTransition(Duration.millis(1000), node);
+// ft.setToValue(1);
+//
+// ParallelTransition pt = new ParallelTransition(tt, ft);
+// pt.play();
}
public static void startBackgroundAnimation(Pane backgroundPane, double screenWidth, double screenHeight) {
- int particleCount = 50;
-
- for (int i = 0; i < particleCount; i++) {
- Label particle = new Label(random.nextBoolean() ? "X" : "O");
- particle.getStyleClass().add("background-particle");
-
- double size = 15 + random.nextInt(50);
- particle.setStyle("-fx-font-size: " + size + "px; -fx-text-fill: rgba(79, 94, 247, " + (0.05 + random.nextDouble() * 0.2) + ");");
-
- particle.setTranslateX(random.nextInt((int) screenWidth));
- particle.setTranslateY(random.nextInt((int) screenHeight));
-
- backgroundPane.getChildren().add(particle);
- animateParticle(particle, screenWidth, screenHeight);
- }
+// int particleCount = 50;
+//
+// for (int i = 0; i < particleCount; i++) {
+// Label particle = new Label(random.nextBoolean() ? "X" : "O");
+// particle.getStyleClass().add("background-particle");
+//
+// double size = 15 + random.nextInt(50);
+// particle.setStyle("-fx-font-size: " + size + "px; -fx-text-fill: rgba(79, 94, 247, " + (0.05 + random.nextDouble() * 0.2) + ");");
+//
+// particle.setTranslateX(random.nextInt((int) screenWidth));
+// particle.setTranslateY(random.nextInt((int) screenHeight));
+//
+// backgroundPane.getChildren().add(particle);
+// animateParticle(particle, screenWidth, screenHeight);
+// }
}
private static void animateParticle(Label particle, double width, double height) {
- TranslateTransition move = new TranslateTransition(Duration.seconds(15 + random.nextInt(15)), particle);
- double endX = particle.getTranslateX() + (random.nextInt(200) - 100); // Drift sideways
- double endY = -100;
-
- move.setToX(endX);
- move.setToY(endY);
- move.setCycleCount(1);
-
- RotateTransition rotate = new RotateTransition(Duration.seconds(5 + random.nextInt(10)), particle);
- rotate.setByAngle(360);
- rotate.setCycleCount(RotateTransition.INDEFINITE);
-
- move.setOnFinished(e -> {
-
- particle.setTranslateY(height + random.nextInt(100));
- particle.setTranslateX(random.nextInt((int) width));
- animateParticle(particle, width, height);
- });
-
- move.play();
- rotate.play();
+// TranslateTransition move = new TranslateTransition(Duration.seconds(15 + random.nextInt(15)), particle);
+// double endX = particle.getTranslateX() + (random.nextInt(200) - 100); // Drift sideways
+// double endY = -100;
+//
+// move.setToX(endX);
+// move.setToY(endY);
+// move.setCycleCount(1);
+//
+// RotateTransition rotate = new RotateTransition(Duration.seconds(5 + random.nextInt(10)), particle);
+// rotate.setByAngle(360);
+// rotate.setCycleCount(RotateTransition.INDEFINITE);
+//
+// move.setOnFinished(e -> {
+//
+// particle.setTranslateY(height + random.nextInt(100));
+// particle.setTranslateX(random.nextInt((int) width));
+// animateParticle(particle, width, height);
+// });
+//
+// move.play();
+// rotate.play();
}
}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 159c9a5..4802061 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -11,6 +11,9 @@
import com.boredxgames.tictactoeclient.domain.services.GameService;
import com.boredxgames.tictactoeclient.domain.services.game.GameBoard;
import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService;
+import java.net.URL;
+import java.util.Objects;
+import java.util.ResourceBundle;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
@@ -29,10 +32,6 @@
import javafx.scene.text.Text;
import javafx.util.Duration;
-import java.net.URL;
-import java.util.Objects;
-import java.util.ResourceBundle;
-
/**
* @author Tasneem
*/
@@ -101,10 +100,11 @@ public class GameController implements Initializable, NavigationParameterAware {
@Override
public void initialize(URL url, ResourceBundle rb) {
+
cells = new Button[][]{
- {cell00, cell01, cell02},
- {cell10, cell11, cell12},
- {cell20, cell21, cell22}
+ {cell00, cell01, cell02},
+ {cell10, cell11, cell12},
+ {cell20, cell21, cell22}
};
gameBoard = new GameBoard();
setupCellHandlers();
@@ -151,19 +151,31 @@ private void applyGameModeSettings() {
// gameService = new OfflinePVEAIService(); // TODO: implement offline pve ai service
}
case ONLINE_PVP -> {
+
opponentTypeLabel.setText("ONLINE PLAYER");
difficultyBadge.setVisible(false);
difficultyBadge.setManaged(false);
changeDifficultyButton.setVisible(false);
- changeDifficultyButton.setManaged(false);
- gameService = OnlinePVPService.getInstance().setMoveListener((move) ->
- {
+ changeDifficultyButton.setManaged(false);
+ System.out.println("before run later in switch");
+
+ gameService = OnlinePVPService.getInstance().setMoveListener((move)
+ -> {
+
Platform.runLater(() -> {
updateCell(move.getCol(), move.getRow(), gameBoard.getCurrentPlayer());
gameBoard.switchPlayer();
enableBoard();
+
+ if (gameService instanceof OnlinePVPService) {
+
+ ((OnlinePVPService) gameService).setIsMyTurn(true);
+ }
});
});
+ System.out.println("before check turn in switch");
+ OnlinePVPService.getInstance().setupOnlineGame();
+
}
}
}
@@ -201,14 +213,32 @@ private void setupButtonHandlers() {
}
private void handleCellClick(int row, int col) {
- if (!gameBoard.isValidMove(row, col)) return;
+ if (!gameBoard.isValidMove(row, col)) {
+ return;
+ }
- char currentPlayer = gameBoard.getCurrentPlayer();
+ if (gameService instanceof OnlinePVPService) {
+ boolean isMyTurn;
+ isMyTurn = ((OnlinePVPService) gameService).checkTurn();
+ if (!isMyTurn) {
+ return;
+ }
+ }
+
+ //TODO switch col and row
+ Move move = new Move(col, row);
- Move move = new Move(row, col);
+ char currentPlayer = gameBoard.getCurrentPlayer();
gameService.makeMove(move, currentPlayer);
updateCell(move.getCol(), move.getRow(), currentPlayer);
+
disableBoard();
+
+ if (gameService instanceof OnlinePVPService) {
+
+ ((OnlinePVPService) gameService).setIsMyTurn(false);
+ }
+
gameBoard.switchPlayer();
Move nextMove = gameService.getNextMove(gameBoard, currentPlayer);
@@ -341,17 +371,17 @@ private void showGameOverModal(GameState state) {
case X_WINS:
modalIcon.setText("emoji_events");
modalTitle.setText("Victory!");
- modalMessage.setText(gameMode == GameMode.OFFLINE_PVE ?
- "The CPU didn't stand a chance against your moves." :
- "Player 1 wins the game!");
+ modalMessage.setText(gameMode == GameMode.OFFLINE_PVE
+ ? "The CPU didn't stand a chance against your moves."
+ : "Player 1 wins the game!");
break;
case O_WINS:
modalIcon.setText("sentiment_dissatisfied");
modalTitle.setText("Defeat!");
- modalMessage.setText(gameMode == GameMode.OFFLINE_PVE ?
- "The CPU outsmarted you this time." :
- "Player 2 wins the game!");
+ modalMessage.setText(gameMode == GameMode.OFFLINE_PVE
+ ? "The CPU outsmarted you this time."
+ : "Player 2 wins the game!");
break;
case DRAW:
@@ -421,4 +451,4 @@ private void enableBoard() {
}
}
}
-}
\ No newline at end of file
+}
From 297c46857b527b865e22dcd72e7ae91685eb613b Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Tue, 13 Jan 2026 19:45:36 +0200
Subject: [PATCH 23/26] Improve move handling and game state checks
Refactored move handling logic for both online and offline modes to ensure moves are validated and applied consistently. Added game state checks after each move to handle game end scenarios immediately. Updated turn indicator and board enabling/disabling logic for better user experience.
---
.../presentation/GameController.java | 93 +++++++++++++------
1 file changed, 65 insertions(+), 28 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 159c9a5..9023b38 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -150,21 +150,28 @@ private void applyGameModeSettings() {
changeDifficultyButton.setManaged(true);
// gameService = new OfflinePVEAIService(); // TODO: implement offline pve ai service
}
- case ONLINE_PVP -> {
- opponentTypeLabel.setText("ONLINE PLAYER");
- difficultyBadge.setVisible(false);
- difficultyBadge.setManaged(false);
- changeDifficultyButton.setVisible(false);
- changeDifficultyButton.setManaged(false);
- gameService = OnlinePVPService.getInstance().setMoveListener((move) ->
- {
- Platform.runLater(() -> {
- updateCell(move.getCol(), move.getRow(), gameBoard.getCurrentPlayer());
- gameBoard.switchPlayer();
- enableBoard();
- });
- });
+ case ONLINE_PVP -> {
+ opponentTypeLabel.setText("ONLINE PLAYER");
+ difficultyBadge.setVisible(false);
+ difficultyBadge.setManaged(false);
+ changeDifficultyButton.setVisible(false);
+ changeDifficultyButton.setManaged(false);
+ gameService = OnlinePVPService.getInstance().setMoveListener((move) -> {
+ Platform.runLater(() -> {
+ if (gameBoard.makeMove(move.getRow(), move.getCol(), gameBoard.getCurrentPlayer())) {
+ updateCell(move.getRow(), move.getCol(), gameBoard.getCurrentPlayer());
+
+ if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
+ handleGameEnd();
+ } else {
+ gameBoard.switchPlayer();
+ updateTurnIndicator();
+ enableBoard();
+ }
}
+ });
+ });
+}
}
}
@@ -200,18 +207,51 @@ private void setupButtonHandlers() {
});
}
- private void handleCellClick(int row, int col) {
- if (!gameBoard.isValidMove(row, col)) return;
-
- char currentPlayer = gameBoard.getCurrentPlayer();
+ private void handleCellClick(int row, int col) {
+ if (!gameBoard.isValidMove(row, col)) return;
- Move move = new Move(row, col);
+ char currentPlayer = gameBoard.getCurrentPlayer();
+
+ // Make the move on the local board first
+ if (!gameBoard.makeMove(row, col, currentPlayer)) {
+ return;
+ }
+
+ Move move = new Move(row, col);
+
+ // For online mode, send the move to server
+ if (gameMode == GameMode.ONLINE_PVP) {
gameService.makeMove(move, currentPlayer);
- updateCell(move.getCol(), move.getRow(), currentPlayer);
+ updateCell(row, col, currentPlayer);
disableBoard();
- gameBoard.switchPlayer();
-
- Move nextMove = gameService.getNextMove(gameBoard, currentPlayer);
+
+ // Check for game end after our move
+ if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
+ handleGameEnd();
+ } else {
+ gameBoard.switchPlayer();
+ updateTurnIndicator();
+ }
+ return;
+ }
+
+ // For offline modes (PVP/PVE)
+ gameService.makeMove(move, currentPlayer);
+ updateCell(row, col, currentPlayer);
+ gameBoard.switchPlayer();
+
+ // Check game state after player move
+ if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
+ handleGameEnd();
+ return;
+ }
+
+ updateTurnIndicator();
+
+ // Handle CPU move for PVE
+ if (gameMode == GameMode.OFFLINE_PVE) {
+ disableBoard();
+ Move nextMove = gameService.getNextMove(gameBoard, gameBoard.getCurrentPlayer());
if (nextMove != null) {
Platform.runLater(() -> {
if (gameBoard.makeMove(nextMove.getRow(), nextMove.getCol(), gameBoard.getCurrentPlayer())) {
@@ -221,16 +261,13 @@ private void handleCellClick(int row, int col) {
} else {
gameBoard.switchPlayer();
updateTurnIndicator();
+ enableBoard();
}
}
});
}
-
- GameState state = gameService.getOutcome(gameBoard);
- if (state != GameState.IN_PROGRESS) {
- handleGameEnd();
- }
}
+}
private void updateCell(int row, int col, char player) {
Button cell = cells[row][col];
From 955d198ffbcb9c6d0b900ca3fdca60cc73d9a9c3 Mon Sep 17 00:00:00 2001
From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com>
Date: Tue, 13 Jan 2026 20:12:34 +0200
Subject: [PATCH 24/26] Update HomeController.java
---
.../tictactoeclient/presentation/HomeController.java | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
index 669a6b2..d7d4851 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java
@@ -43,15 +43,7 @@ public void initialize(URL url, ResourceBundle rb) {
if(scoreLabel != null) scoreLabel.setText("Score: " + currentUser.getScore());
}
- Platform.runLater(() -> {
- if (backgroundPane != null) {
- double w = rootStack.getWidth() > 0 ? rootStack.getWidth() : 1280;
- double h = rootStack.getHeight() > 0 ? rootStack.getHeight() : 800;
- BackgroundHomeAnimation.startBackgroundAnimation(backgroundPane, w, h);
- }
- javafx.scene.Node viewport = rootStack.lookup(".viewport");
- if (viewport != null) viewport.setStyle("-fx-background-color: rgba(0,0,0,0);");
- });
+
OnlinGameSession.getInstance().requestLeaderboard();
OnlinGameSession.getInstance().requestAvailablePlayers();
From 1e40e5dd3104d3b335f1b3e81daf4b51fa82e2db Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Tue, 13 Jan 2026 23:45:09 +0200
Subject: [PATCH 25/26] feat: enhance online multiplayer functionality and
improve game state management
---
.../services/communication/MessageRouter.java | 6 +-
.../domain/services/game/GameBoard.java | 7 +-
.../services/game/OfflinePVEAIService.java | 1 +
.../services/game/OfflinePVPService.java | 3 +-
.../services/game/OnlinePVPService.java | 23 +-
.../presentation/GameController.java | 249 +++++++++---------
.../OfflineModeSelectionController.java | 6 +-
7 files changed, 143 insertions(+), 152 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
index 827bbc0..b523275 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java
@@ -143,7 +143,11 @@ private void handleEvent(Message msg) {
}
case GAME_START -> {
OnlineGameState.info = gson.fromJson(msg.getData(), GameStartInfo.class);
- NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, GameMode.ONLINE_PVP);
+ String player1Name = "You";
+ String player2Name = "Opponent";
+ NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, new GameNavigationParams(
+ player1Name,player2Name, GameMode.ONLINE_PVP
+ ));
}
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java
index 900730b..210766f 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java
@@ -22,6 +22,7 @@ public class GameBoard {
private GameState gameState;
private int movesCount;
+
public GameBoard() {
this(PLAYER_X);
}
@@ -58,6 +59,10 @@ public boolean makeMove(int row, int col) {
}
public boolean makeMove(int row, int col, char player) {
+ if (player != currentPlayer) {
+ return false;
+ }
+
if (!isValidMove(row, col)) {
return false;
}
@@ -67,7 +72,7 @@ public boolean makeMove(int row, int col, char player) {
updateGameState();
if (gameState == GameState.IN_PROGRESS) {
- currentPlayer = (player == PLAYER_X) ? PLAYER_O : PLAYER_X;
+ currentPlayer = (player == PLAYER_X) ? PLAYER_O : PLAYER_X;
}
return true;
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java
index 16c299c..2f93b4b 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java
@@ -7,6 +7,7 @@
import com.boredxgames.tictactoeclient.domain.model.GameState;
import com.boredxgames.tictactoeclient.domain.model.Move;
import com.boredxgames.tictactoeclient.domain.services.AIService;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
/**
*
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java
index 18c3697..c667efc 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java
@@ -6,12 +6,13 @@
import com.boredxgames.tictactoeclient.domain.model.GameState;
import com.boredxgames.tictactoeclient.domain.model.Move;
+import com.boredxgames.tictactoeclient.domain.services.GameService;
/**
*
* @author sheri
*/
-public class OfflinePVPService implements GameService{
+public class OfflinePVPService implements GameService {
@Override
public void makeMove(Move move, char currentPlayer) {
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
index 30e143a..b6eedb7 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java
@@ -12,28 +12,16 @@
public class OnlinePVPService implements GameService {
private static OnlinePVPService instance;
- private boolean isMyTurn = false;
- public void setupOnlineGame() {
+ public boolean shouldPlayFirst() {
System.out.println(" Check equality"+OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId()));
if (OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId())) {
- isMyTurn = true;
+ return true;
}
- }
-
- public void setIsMyTurn(boolean isMyTurn) {
- this.isMyTurn = isMyTurn;
- }
-
- public boolean isIsMyTurn() {
- return isMyTurn;
- }
-
- public boolean checkTurn() {
- return isMyTurn;
+ return false;
}
public static OnlinePVPService getInstance() {
@@ -80,6 +68,11 @@ public void makeMove(Move move, char currentPlayer) {
connectionManager.sendMessage(msg);
}
+ @Override
+ public void makeMove(Move move, char currentPlayer, GameBoard board) {
+
+ }
+
@Override
public Move getNextMove(GameBoard board, char currentPlayer) {
// In online play, we don't calculate the next move locally.
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 20f453a..891ebde 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -8,15 +8,12 @@
import com.boredxgames.tictactoeclient.domain.model.GameNavigationParams;
import com.boredxgames.tictactoeclient.domain.model.GameState;
import com.boredxgames.tictactoeclient.domain.model.Move;
+import com.boredxgames.tictactoeclient.domain.services.AIService;
import com.boredxgames.tictactoeclient.domain.services.GameService;
import com.boredxgames.tictactoeclient.domain.services.game.GameBoard;
-import com.boredxgames.tictactoeclient.domain.services.AIService;
-import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService;
-import java.net.URL;
-import java.util.Objects;
-import java.util.ResourceBundle;
import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVEAIService;
import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVPService;
+import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
@@ -35,9 +32,10 @@
import javafx.scene.text.Text;
import javafx.util.Duration;
-/**
- * @author Tasneem
- */
+import java.net.URL;
+import java.util.Objects;
+import java.util.ResourceBundle;
+
public class GameController implements Initializable, NavigationParameterAware {
public GridPane gameGrid;
@@ -100,14 +98,15 @@ public class GameController implements Initializable, NavigationParameterAware {
private String player2Name = "Player 2";
private GameService gameService;
+ private char localPlayerId;
@Override
public void initialize(URL url, ResourceBundle rb) {
cells = new Button[][]{
- {cell00, cell01, cell02},
- {cell10, cell11, cell12},
- {cell20, cell21, cell22}
+ {cell00, cell01, cell02},
+ {cell10, cell11, cell12},
+ {cell20, cell21, cell22}
};
gameBoard = new GameBoard();
setupCellHandlers();
@@ -121,7 +120,6 @@ public void setNavigationParameter(Object parameter) {
this.player2Name = params.player2();
this.gameMode = params.mode();
} else {
- // Default
this.gameMode = GameMode.OFFLINE_PVP;
this.player1Name = "Player 1";
this.player2Name = "Player 2";
@@ -146,6 +144,7 @@ private void applyGameModeSettings() {
difficultyBadge.setManaged(false);
changeDifficultyButton.setVisible(false);
changeDifficultyButton.setManaged(false);
+
gameService = new OfflinePVPService();
}
case OFFLINE_PVE -> {
@@ -154,30 +153,45 @@ private void applyGameModeSettings() {
difficultyBadge.setManaged(true);
changeDifficultyButton.setVisible(true);
changeDifficultyButton.setManaged(true);
+
gameService = new OfflinePVEAIService();
}
- case ONLINE_PVP -> {
- opponentTypeLabel.setText("ONLINE PLAYER");
- difficultyBadge.setVisible(false);
- difficultyBadge.setManaged(false);
- changeDifficultyButton.setVisible(false);
- changeDifficultyButton.setManaged(false);
- gameService = OnlinePVPService.getInstance().setMoveListener((move) -> {
- Platform.runLater(() -> {
- if (gameBoard.makeMove(move.getRow(), move.getCol(), gameBoard.getCurrentPlayer())) {
- updateCell(move.getRow(), move.getCol(), gameBoard.getCurrentPlayer());
+ case ONLINE_PVP -> {
+ opponentTypeLabel.setText("ONLINE PLAYER");
+ difficultyBadge.setVisible(false);
+ difficultyBadge.setManaged(false);
+ changeDifficultyButton.setVisible(false);
+ changeDifficultyButton.setManaged(false);
- if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
- handleGameEnd();
- } else {
- gameBoard.switchPlayer();
- updateTurnIndicator();
+ boolean amIPlayer1 = OnlinePVPService.getInstance().shouldPlayFirst();
+ localPlayerId = amIPlayer1 ? GameBoard.PLAYER_X : GameBoard.PLAYER_O;
+
+ System.out.println("I am Player: " + localPlayerId);
+
+ gameService = OnlinePVPService.getInstance().setMoveListener((move) -> {
+ Platform.runLater(() -> {
+ char remotePlayer = (localPlayerId == GameBoard.PLAYER_X) ? GameBoard.PLAYER_O : GameBoard.PLAYER_X;
+
+ if (gameBoard.makeMove(move.getRow(), move.getCol(), remotePlayer)) {
+ updateCell(move.getRow(), move.getCol(), remotePlayer);
+
+ if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
+ handleGameEnd();
+ } else {
+ updateTurnIndicator();
+ enableBoard();
+ }
+ }
+ });
+ });
+
+ if (amIPlayer1) {
enableBoard();
+ } else {
+ disableBoard();
}
+ updateTurnIndicator();
}
- });
- });
-}
}
}
@@ -196,7 +210,7 @@ private void setupButtonHandlers() {
playAgainButton.setOnAction(e -> resetGame());
mainMenuButton.setOnAction(e -> {
- NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE_ALL); // TODO: change to mode selection screen
+ NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE_ALL);
});
backButton.setOnAction(e -> {
@@ -209,111 +223,90 @@ private void setupButtonHandlers() {
});
changeDifficultyButton.setOnAction(e -> {
- // TODO change difficulty
});
}
private void handleCellClick(int row, int col) {
- if (!gameBoard.isValidMove(row, col)) return;
+ if (!gameBoard.isValidMove(row, col)) {
+ return;
+ }
- char currentPlayer = gameBoard.getCurrentPlayer();
+ if (gameMode == GameMode.ONLINE_PVP && gameBoard.getCurrentPlayer() != localPlayerId) {
+ return;
+ }
switch (gameMode) {
- case OFFLINE_PVP -> {
- // Player vs Player
-
- gameBoard.makeMove(row, col, currentPlayer);
- updateCell(row, col, currentPlayer);
-
- if (!checkGameEnd()) {
+ case OFFLINE_PVP -> handleOfflinePvp(row, col);
+ case OFFLINE_PVE -> handleOfflinePve(row, col);
+ case ONLINE_PVP -> handleOnlinePvp(row, col);
+ }
+ }
- updateTurnIndicator();
- }
- return;
- }
+ private void handleOfflinePvp(int row, int col) {
+ char currentPlayer = gameBoard.getCurrentPlayer();
- case OFFLINE_PVE -> {
- // Player X moves
- gameBoard.makeMove(row, col, GameBoard.PLAYER_X);
- updateCell(row, col, GameBoard.PLAYER_X);
- if (checkGameEnd()) {
- return;
- }
+ performMove(row, col, currentPlayer);
- // AI O moves
- disableBoard();
- PauseTransition pause = new PauseTransition(Duration.millis(500)); // AI "thinking"
- pause.setOnFinished(e -> {
- Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O);
- gameService.makeMove(aiMove, GameBoard.PLAYER_O, gameBoard);
-
- if (aiMove != null) {
- updateCell(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O);
- }
-
- if (!checkGameEnd()) {
- enableBoard();
- gameBoard.switchPlayer();
- updateTurnIndicator();
- }
- });
- pause.play();
- return;
- }
+ if (!checkGameEnd()) {
+ gameBoard.switchPlayer();
+ updateTurnIndicator();
}
+ }
+
+ private void handleOfflinePve(int row, int col) {
+ performMove(row, col, GameBoard.PLAYER_X);
- if (!gameBoard.makeMove(row, col, currentPlayer)) {
+ if (checkGameEnd()) {
return;
}
- Move move = new Move(row, col);
+ scheduleAiTurn();
+ }
- if (gameMode == GameMode.ONLINE_PVP) {
- gameService.makeMove(move, currentPlayer);
- updateCell(row, col, currentPlayer);
- disableBoard();
+ private void handleOnlinePvp(int row, int col) {
+ performMove(row, col, localPlayerId);
- if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
- handleGameEnd();
- } else {
- gameBoard.switchPlayer();
- updateTurnIndicator();
- }
- return;
- }
+ Move move = new Move(row, col);
+ gameService.makeMove(move, localPlayerId);
- gameService.makeMove(move, currentPlayer);
- updateCell(row, col, currentPlayer);
- gameBoard.switchPlayer();
+ disableBoard();
if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
handleGameEnd();
- return;
+ } else {
+ updateTurnIndicator();
}
+ }
- updateTurnIndicator();
+ private void performMove(int row, int col, char player) {
+ gameBoard.makeMove(row, col, player);
+ updateCell(row, col, player);
+ }
- if (gameMode == GameMode.OFFLINE_PVE) {
- disableBoard();
- Move nextMove = gameService.getNextMove(gameBoard, gameBoard.getCurrentPlayer());
- if (nextMove != null) {
- Platform.runLater(() -> {
- if (gameBoard.makeMove(nextMove.getRow(), nextMove.getCol(), gameBoard.getCurrentPlayer())) {
- updateCell(nextMove.getRow(), nextMove.getCol(), gameBoard.getCurrentPlayer());
- if (gameBoard.getGameState() != GameState.IN_PROGRESS) {
- handleGameEnd();
- } else {
- gameBoard.switchPlayer();
- updateTurnIndicator();
- enableBoard();
- }
- }
- });
- }
- }
+ private void scheduleAiTurn() {
+ disableBoard();
+
+ PauseTransition pause = new PauseTransition(Duration.millis(500));
+ pause.setOnFinished(e -> executeAiMove());
+ pause.play();
}
+
+ private void executeAiMove() {
+ Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O);
+
+ if (aiMove != null) {
+ gameService.makeMove(aiMove, GameBoard.PLAYER_O, gameBoard);
+ performMove(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O);
+ }
+
+ if (!checkGameEnd()) {
+ enableBoard();
+ gameBoard.switchPlayer();
+ updateTurnIndicator();
+ }
}
+
private boolean checkGameEnd() {
GameState state = gameBoard.getGameState();
if (state != GameState.IN_PROGRESS) {
@@ -322,6 +315,7 @@ private boolean checkGameEnd() {
}
return false;
}
+
private void updateCell(int row, int col, char player) {
Button cell = cells[row][col];
@@ -347,27 +341,12 @@ private void updateCell(int row, int col, char player) {
}
private void updateTurnIndicator() {
- char current = gameBoard.getCurrentPlayer();
- boolean isPlayerX = (current == GameBoard.PLAYER_X);
+ char currentPlayer = gameBoard.getCurrentPlayer();
- if (gameMode == GameMode.OFFLINE_PVP) {
- if (isPlayerX) {
- setActiveCard(playerCard, opponentCard);
- } else {
- setActiveCard(opponentCard, playerCard);
- }
- } else if (gameMode == GameMode.OFFLINE_PVE) {
- if (isPlayerX) {
- setActiveCard(playerCard, opponentCard);
- } else {
- setActiveCard(opponentCard, playerCard);
- }
+ if (currentPlayer == GameBoard.PLAYER_X) {
+ setActiveCard(playerCard, opponentCard);
} else {
- if (isPlayerTurn) {
- setActiveCard(playerCard, opponentCard);
- } else {
- setActiveCard(opponentCard, playerCard);
- }
+ setActiveCard(opponentCard, playerCard);
}
}
@@ -490,8 +469,18 @@ public void resetGame() {
}
modalOverlay.setVisible(false);
+
updateTurnIndicator();
- enableBoard();
+
+ if (gameMode == GameMode.ONLINE_PVP) {
+ if (localPlayerId == GameBoard.PLAYER_X) {
+ enableBoard();
+ } else {
+ disableBoard();
+ }
+ } else {
+ enableBoard();
+ }
}
private void disableBoard() {
@@ -513,4 +502,4 @@ private void enableBoard() {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
index 6a2c593..99f8601 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java
@@ -29,14 +29,12 @@ private void handleBackToMainMenu(ActionEvent event) {
@FXML
private void handlePlayerVsPlayer(MouseEvent event) {
- // TODO: Navigate to PvP game screen
- System.out.println("Player vs Player mode selected");
+ NavigationManager.navigate(Screens.PVP_SETUP, NavigationAction.PUSH);
}
@FXML
private void handlePlayerVsCPU(MouseEvent event) {
- // TODO: Navigate to PvE game screen
- System.out.println("Player vs CPU mode selected");
+ NavigationManager.navigate(Screens.DifficultySelection, NavigationAction.PUSH);
}
}
\ No newline at end of file
From 46e9a747ddc4f1cc95f36a8ceffafa024aa62562 Mon Sep 17 00:00:00 2001
From: "ZEIAD-LAPTOP\\zeiad"
Date: Wed, 14 Jan 2026 00:11:53 +0200
Subject: [PATCH 26/26] feat: enhance authentication dialog styling and
implement replay functionality
---
.../domain/managers/navigation/Screens.java | 1 -
.../AuthenticationController.java | 31 ++++++++++++++++++-
.../presentation/GameController.java | 30 +++++++++++++-----
.../css/features/authentication_screen.css | 21 +++++++++++++
4 files changed, 73 insertions(+), 10 deletions(-)
diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
index cc73466..2d39688 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java
@@ -11,7 +11,6 @@ public enum Screens {
GAME("game_screen"),
Home("home"),
PVP_SETUP("pvp_setup"),
- GAME("game_screen"),
DifficultySelection("difficulty_selection"),
RECORDINGS("RecordingsListScreen");
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
index 2e095b4..bebe657 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java
@@ -121,10 +121,39 @@ public static void showUserAlert(String message) {
alert.setHeaderText(null);
alert.setContentText(message);
+ DialogPane dialogPane = alert.getDialogPane();
+
+ // 1. Style the main pane
+ dialogPane.setStyle(
+ "-fx-background-color: #1e1e1e; " +
+ "-fx-border-color: #333333; " +
+ "-fx-border-width: 2px;"
+ );
+
+ // 2. Style all labels (Content text)
+ dialogPane.lookupAll(".label").forEach(node ->
+ node.setStyle("-fx-text-fill: #e0e0e0; -fx-font-family: 'Segoe UI';")
+ );
+
+ // 3. Style the buttons
+ dialogPane.lookupAll(".button").forEach(node -> {
+ node.setStyle(
+ "-fx-background-color: #3c3f41; " +
+ "-fx-text-fill: white; " +
+ "-fx-background-radius: 4; " +
+ "-fx-cursor: hand;"
+ );
+
+ // Add simple hover effect via code
+ node.setOnMouseEntered(e -> node.setStyle("-fx-background-color: #4b4d4d; -fx-text-fill: white; -fx-background-radius: 4;"));
+ node.setOnMouseExited(e -> node.setStyle("-fx-background-color: #3c3f41; -fx-text-fill: white; -fx-background-radius: 4;"));
+ });
+
+ // Set the owner window
javafx.stage.Window.getWindows().stream()
.filter(javafx.stage.Window::isShowing)
.findFirst()
- .ifPresent(window -> alert.initOwner(window));
+ .ifPresent(alert::initOwner);
alert.showAndWait().ifPresent(type -> {
if ("Server is out of service".equals(message) || "Internal Server Error".equals(message)) {
diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
index 97c474d..7b4e3df 100644
--- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
+++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java
@@ -24,6 +24,7 @@
import javafx.animation.PauseTransition;
import javafx.animation.Timeline;
+import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
@@ -324,13 +325,6 @@ private void executeAiMove() {
enableBoard();
gameBoard.switchPlayer();
updateTurnIndicator();
- case OFFLINE_PVP -> {
- gameBoard.makeMove(row, col, currentPlayer);
- updateCell(row, col, currentPlayer);
- if (!checkGameEnd()) {
- updateTurnIndicator();
- }
- }
}
}
@@ -371,7 +365,7 @@ private void updateTurnIndicator() {
setActiveCard(opponentCard, playerCard);
}
if (gameMode == GameMode.OFFLINE_PVP || gameMode == GameMode.OFFLINE_PVE || gameMode == GameMode.REPLAY) {
- if (isPlayerX) {
+ if (localPlayerId == GameBoard.PLAYER_X) {
setActiveCard(playerCard, opponentCard);
} else {
setActiveCard(opponentCard, playerCard);
@@ -510,4 +504,24 @@ private void enableBoard() {
}
}
}
+
+ private void startReplay() {
+ disableBoard();
+ Timeline timeline = new Timeline();
+ int delay = 0;
+
+ for (var move : replayData.moves()) {
+ delay += 1000;
+ KeyFrame frame = new KeyFrame(Duration.millis(delay), e -> {
+ gameBoard.forceMove(move.row(), move.col(), move.player());
+ updateCell(move.row(), move.col(), move.player());
+
+ gameBoard.switchPlayer();
+ updateTurnIndicator();
+ checkGameEnd();
+ });
+ timeline.getKeyFrames().add(frame);
+ }
+ timeline.play();
+ }
}
\ No newline at end of file
diff --git a/src/main/resources/css/features/authentication_screen.css b/src/main/resources/css/features/authentication_screen.css
index adca663..8d82b96 100644
--- a/src/main/resources/css/features/authentication_screen.css
+++ b/src/main/resources/css/features/authentication_screen.css
@@ -122,3 +122,24 @@
-fx-text-fill: #4f5ef7;
-fx-underline: true;
}
+
+.dialog-pane {
+ -fx-background-color: #2b2b2b;
+}
+
+.dialog-pane .label {
+ -fx-text-fill: white;
+}
+
+.dialog-pane:header .header-panel {
+ -fx-background-color: #3c3f41;
+}
+
+.dialog-pane .button {
+ -fx-background-color: #4a4a4a;
+ -fx-text-fill: white;
+}
+
+.dialog-pane .button:hover {
+ -fx-background-color: #5a5a5a;
+}
\ No newline at end of file