From daa5a81e873b43054d34e1952e39a8a24b59974b Mon Sep 17 00:00:00 2001 From: Mahmoud Raafat <100778020+MahmoudRafaat@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:14:15 +0200 Subject: [PATCH] Add online game end handling and improve error alerts Introduces GameEndInfo model and implements online game end reporting in OnlinePVPService and GameController. Refactors MessageRouter to support error alerts in GameController and centralizes game start handling. Improves move deserialization and updates game over modal logic for online play. --- .../domain/model/GameEndInfo.java | 32 ++++ .../services/communication/MessageRouter.java | 71 +++++---- .../services/game/OnlinePVPService.java | 35 ++++- .../presentation/GameController.java | 144 +++++++++++++----- 4 files changed, 209 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/model/GameEndInfo.java diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameEndInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameEndInfo.java new file mode 100644 index 0000000..33e9195 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameEndInfo.java @@ -0,0 +1,32 @@ +/* + * 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 GameEndInfo { + + private String roomId; + private String winnerId; // If this is null, it means DRAW + + public GameEndInfo() { + } + + public GameEndInfo(String roomId, String winnerId) { + this.roomId = roomId; + this.winnerId = winnerId; + } + + public String getRoomId() { + return roomId; + } + + public String getWinnerId() { + return winnerId; + } +} + 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 b523275..ab0659d 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 @@ -16,6 +16,7 @@ import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService; import com.boredxgames.tictactoeclient.presentation.AuthenticationController; +import com.boredxgames.tictactoeclient.presentation.GameController; import com.boredxgames.tictactoeclient.presentation.HomeController; import com.google.gson.Gson; import javafx.application.Platform; @@ -26,6 +27,8 @@ public class MessageRouter { private static ServerConnectionManager connection; private Gson gson = new Gson(); private static HomeController homeController; + private static GameController gameController; + private MessageRouter() { connection = ServerConnectionManager.getInstance(); @@ -56,6 +59,9 @@ public static void setHomeController(HomeController controller) { homeController = controller; } + public static void setGameController(GameController controller) { + gameController = controller; + } private Message handleRequest(Message msg) { Action action = msg.getHeader().getAction(); @@ -73,27 +79,20 @@ private Message handleRequest(Message msg) { private void handleResponse(Message msg) { Action action = msg.getHeader().getAction(); - System.out.println(action); + System.out.println("Response Action: " + action); switch (action) { case LOGIN_SUCCESS -> { System.out.println("Login Success"); 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 -> { - System.out.println("Registration success"); - AuthResponseEntity responseData = gson.fromJson(msg.getData(), AuthResponseEntity.class); AuthenticationController.showUserAlert("Registration success"); - } 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."); @@ -103,6 +102,7 @@ private void handleResponse(Message msg) { if (homeController != null) { Platform.runLater(() -> homeController.updatePlayersList(info)); } + } case GAME_RESPONSE -> { GameResponseInfo info = gson.fromJson(msg.getData(), GameResponseInfo.class); @@ -116,22 +116,20 @@ private void handleResponse(Message msg) { Platform.runLater(() -> homeController.updateLeaderboardUI(info)); } } + case GAME_START -> { + handleGameStart(msg); + } case SEND_GAME_UPDATE -> { MoveInfo moveInfo = gson.fromJson(msg.getData(), MoveInfo.class); OnlinePVPService.onIncomingMove(moveInfo); } - - default -> { - System.out.println("Unknown Action: " + action); - - } + default -> System.out.println("Unknown Action: " + action); } - ; } - private void handleEvent(Message msg) { +private void handleEvent(Message msg) { Action action = msg.getHeader().getAction(); - System.out.println(action); + System.out.println("Event Action: " + action); switch (action) { case REQUEST_GAME -> { @@ -139,24 +137,25 @@ private void handleEvent(Message msg) { if (homeController != null) { Platform.runLater(() -> homeController.showIncomingGameRequest(info)); } - } case GAME_START -> { - OnlineGameState.info = gson.fromJson(msg.getData(), GameStartInfo.class); - String player1Name = "You"; - String player2Name = "Opponent"; - NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, new GameNavigationParams( - player1Name,player2Name, GameMode.ONLINE_PVP - )); - - } - - default -> { - System.out.println("Unknown Action: " + action); - + handleGameStart(msg); } + default -> System.out.println("Unknown Action: " + action); } - ; + } + + // *** NEW HELPER METHOD *** + private void handleGameStart(Message msg) { + OnlineGameState.info = gson.fromJson(msg.getData(), GameStartInfo.class); + String player1Name = "You"; + String player2Name = "Opponent"; + + Platform.runLater(() -> { + NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, new GameNavigationParams( + player1Name, player2Name, GameMode.ONLINE_PVP + )); + }); } private void handleError(Message msg) { @@ -179,6 +178,10 @@ private void handleError(Message msg) { if (homeController != null) { Platform.runLater(() -> homeController.showErrorAlert(errorMessage)); } + if (gameController != null) { + Platform.runLater(() -> gameController.showErrorAlert(errorMessage)); + } + } case PLAYER_BUSY -> { @@ -187,6 +190,7 @@ private void handleError(Message msg) { if (homeController != null) { Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); } + } case PENDING_REQUEST_EXISTS -> { String errorMsg = "You already have a request pending."; @@ -206,10 +210,12 @@ private void handleError(Message msg) { case ROOM_NOT_FOUND -> { 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)); } + if (gameController != null) { + Platform.runLater(() -> gameController.showErrorAlert(errorMsg)); + } } case INVALID_OPPONENT -> { @@ -218,6 +224,9 @@ private void handleError(Message msg) { homeController.dismissIncomingRequest(); Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); } + if (gameController != null) { + Platform.runLater(() -> gameController.showErrorAlert(errorMsg)); + } } case INVALID_CREDENTIAL -> { System.out.println("INVALID_CREDENTIAL"); 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 b6eedb7..5f163c5 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 @@ -1,5 +1,8 @@ package com.boredxgames.tictactoeclient.domain.services.game; +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.*; import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; import com.boredxgames.tictactoeclient.domain.services.GameService; @@ -42,9 +45,18 @@ public OnlinePVPService setMoveListener(Consumer listener) { return this; } - public static void onIncomingMove(MoveInfo moveInfo) { + public static void onIncomingMove(MoveInfo moveInfo) { Gson gson = new Gson(); - Move move = gson.fromJson(moveInfo.getMove(), Move.class); + Object moveData = moveInfo.getMove(); + Move move; + + if (moveData instanceof String) { + move = gson.fromJson((String) moveData, Move.class); + } else { + String json = gson.toJson(moveData); + move = gson.fromJson(json, Move.class); + } + if (moveListener != null) { moveListener.accept(move); } @@ -56,10 +68,8 @@ 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, @@ -67,7 +77,24 @@ public void makeMove(Move move, char currentPlayer) { ); connectionManager.sendMessage(msg); } + + public void sendGameEnd(String winnerId) { + ServerConnectionManager connectionManager = ServerConnectionManager.getInstance(); + + if (OnlineGameState.info == null) return; + String roomId = OnlineGameState.info.getRoomId(); + GameEndInfo endInfo = new GameEndInfo(roomId, winnerId); + + Message msg = Message.createMessage( + MessageType.RESPONSE, + Action.GAME_END, + endInfo + ); + + connectionManager.sendMessage(msg); + NavigationManager.navigate(Screens.Home, NavigationAction.REPLACE); + } @Override public void makeMove(Move move, char currentPlayer, GameBoard board) { diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java index 7b4e3df..8065a03 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java @@ -16,6 +16,8 @@ import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVPService; import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService; import com.boredxgames.tictactoeclient.domain.model.GameRecord; +import com.boredxgames.tictactoeclient.domain.model.OnlineGameState; +import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter; import com.boredxgames.tictactoeclient.domain.services.storage.GameRecordingService; import java.net.URL; import java.util.Objects; @@ -97,6 +99,27 @@ public void initialize(URL url, ResourceBundle rb) { gameBoard = new GameBoard(); setupCellHandlers(); setupButtonHandlers(); + MessageRouter.setGameController(this); + } + public void showErrorAlert(String message) { + Platform.runLater(() -> { + modalIcon.setText("error"); + modalTitle.setText("Connection Error"); + modalMessage.setText(message); + + playAgainButton.setVisible(false); + playAgainButton.setManaged(false); + + if (saveGameButton != null) { + saveGameButton.setVisible(false); + saveGameButton.setManaged(false); + } + + mainMenuButton.setVisible(true); + mainMenuButton.setManaged(true); + modalOverlay.setVisible(true); + disableBoard(); + }); } @Override @@ -270,7 +293,6 @@ private void handleOfflinePvp(int row, int col) { performMove(row, col, currentPlayer); if (!checkGameEnd()) { - gameBoard.switchPlayer(); updateTurnIndicator(); } } @@ -313,20 +335,19 @@ private void scheduleAiTurn() { pause.play(); } - private void executeAiMove() { - Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O); + 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 (aiMove != null) { + + performMove(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O); + } - if (!checkGameEnd()) { - enableBoard(); - gameBoard.switchPlayer(); - updateTurnIndicator(); - } + if (!checkGameEnd()) { + enableBoard(); + updateTurnIndicator(); } +} @@ -378,6 +399,8 @@ private void setActiveCard(VBox activeCard, VBox inactiveCard) { inactiveCard.getStyleClass().remove("active-card"); } + // In GameController.java, update handleGameEnd: + private void handleGameEnd() { GameState state = gameBoard.getGameState(); @@ -389,57 +412,102 @@ private void handleGameEnd() { cells[row][col].getStyleClass().add("cell-winning"); } } + char winnerChar = GameBoard.EMPTY; + if (state == GameState.X_WINS) winnerChar = GameBoard.PLAYER_X; + else if (state == GameState.O_WINS) winnerChar = GameBoard.PLAYER_O; - if (state == GameState.X_WINS) { + if (winnerChar == GameBoard.PLAYER_X) { if (gameMode != GameMode.REPLAY) { playerScore++; playerScoreLabel.setText(String.valueOf(playerScore)); } - playVictoryVideo(); - } else if (state == GameState.O_WINS) { + } else if (winnerChar == GameBoard.PLAYER_O) { if (gameMode != GameMode.REPLAY) { opponentScore++; opponentScoreLabel.setText(String.valueOf(opponentScore)); } } + if (gameMode == GameMode.ONLINE_PVP) { + + + String winnerId = "DRAW"; + if (state == GameState.X_WINS) { + // Player 1 is always X + winnerId = OnlineGameState.info.getPlayer1(); + } else if (state == GameState.O_WINS) { + winnerId = OnlineGameState.info.getPlayer2(); + } + + OnlinePVPService.getInstance().sendGameEnd(winnerId); + + // Play video check (Keep existing logic) + boolean amIWinner = (winnerChar == localPlayerId); + if (amIWinner) playVictoryVideo(); + + } else { + // Offline Video Logic (Keep existing logic) + if (winnerChar == GameBoard.PLAYER_X) playVictoryVideo(); + } + + // 4. Show Modal (Keep existing code) PauseTransition pause = new PauseTransition(Duration.millis(800)); pause.setOnFinished(e -> showGameOverModal(state)); pause.play(); } - private void showGameOverModal(GameState state) { - switch (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!"); - 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!"); - break; - case DRAW: - modalIcon.setText("handshake"); - modalTitle.setText("Draw!"); - modalMessage.setText("It's a tie! Both players played well."); - break; + private void showGameOverModal(GameState state) { + String title = ""; + String message = ""; + String icon = ""; + + if (state == GameState.DRAW) { + icon = "handshake"; + title = "Draw!"; + message = "It's a tie! Both players played well."; + } else { + char winner = (state == GameState.X_WINS) ? GameBoard.PLAYER_X : GameBoard.PLAYER_O; + + if (gameMode == GameMode.ONLINE_PVP) { + if (winner == localPlayerId) { + icon = "emoji_events"; + title = "Victory!"; + message = "You won the match!"; + } else { + icon = "sentiment_dissatisfied"; + title = "Defeat!"; + message = "Better luck next time."; + } + } + else { + if (winner == GameBoard.PLAYER_X) { + icon = "emoji_events"; + title = "Victory!"; + message = (gameMode == GameMode.OFFLINE_PVE) + ? "The CPU didn't stand a chance." + : "Player 1 wins!"; + } else { + icon = "sentiment_dissatisfied"; + title = (gameMode == GameMode.OFFLINE_PVE) ? "Defeat!" : "Player 2 Wins!"; + message = (gameMode == GameMode.OFFLINE_PVE) + ? "The CPU outsmarted you." + : "Player 2 takes the round."; + } + } } + modalIcon.setText(icon); + modalTitle.setText(title); + modalMessage.setText(message); + if (saveGameButton != null) { saveGameButton.setText("Save Replay"); saveGameButton.setDisable(false); - boolean canSave = (gameMode == GameMode.OFFLINE_PVE || gameMode == GameMode.OFFLINE_PVP); saveGameButton.setVisible(canSave); saveGameButton.setManaged(canSave); } - + if (gameMode == GameMode.REPLAY) { playAgainButton.setVisible(false); playAgainButton.setManaged(false);