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 d80f80b..930c306 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 @@ -10,7 +10,8 @@ public enum Screens { Home("home"), PVP_SETUP("pvp_setup"), GAME("game_screen"), - DifficultySelection("difficulty_selection"); + DifficultySelection("difficulty_selection"), + RECORDINGS("RecordingsListScreen"); private final String name; diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameMode.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameMode.java index 2b62b91..8d9e92e 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameMode.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameMode.java @@ -3,5 +3,6 @@ public enum GameMode { OFFLINE_PVP, OFFLINE_PVE, - ONLINE_PVP + ONLINE_PVP, + REPLAY } \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameNavigationParams.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameNavigationParams.java index b932cdf..aa40c1c 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameNavigationParams.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameNavigationParams.java @@ -2,4 +2,14 @@ /** * @author Tasneem */ -public record GameNavigationParams(String player1, String player2, GameMode mode) { } \ No newline at end of file + +public record GameNavigationParams( + String player1, + String player2, + GameMode mode, + GameRecord replayData +) { + public GameNavigationParams(String player1, String player2, GameMode mode) { + this(player1, player2, mode, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRecord.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRecord.java new file mode 100644 index 0000000..39447a0 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRecord.java @@ -0,0 +1,25 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Record.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +import java.util.List; + +/** + * + * @author Hazem + */ +public record GameRecord( + String date, + String player1, + String player2, + char winner, + List moves +) { + public String getResultDescription() { + if (winner == 'X') return player1 + " (X) Won"; + if (winner == 'O') return player2 + " (O) Won"; + return "Draw"; + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/RecordedMove.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/RecordedMove.java new file mode 100644 index 0000000..eb6e1ac --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/RecordedMove.java @@ -0,0 +1,12 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Record.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +/** + * + * @author Hazem + */ +public record RecordedMove(int row, int col, char player) { +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameRecordingService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameRecordingService.java new file mode 100644 index 0000000..d8b2449 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameRecordingService.java @@ -0,0 +1,106 @@ +/* + * 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.storage; + +import com.boredxgames.tictactoeclient.domain.model.GameRecord; +import com.boredxgames.tictactoeclient.domain.model.RecordedMove; +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hazem + */ +public class GameRecordingService { + + private static final String HEADER_SIGNATURE = "TICTACTOE_REC_V1"; + private static final String DIRECTORY_PATH = "recordings"; + + public void saveGame(GameBoard board, String p1Name, String p2Name, String filename) throws IOException { + File directory = new File(DIRECTORY_PATH); + if (!directory.exists()) { + directory.mkdirs(); + } + + if (!filename.endsWith(".dat")) { + filename += ".dat"; + } + + File file = new File(directory, filename); + + try (DataOutputStream dos = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(file)))) { + + dos.writeUTF(HEADER_SIGNATURE); + + dos.writeUTF(LocalDateTime.now().toString()); + dos.writeUTF(p1Name); + dos.writeUTF(p2Name); + + // 3. Winner + dos.writeChar(board.getWinner()); + + // 4. Moves + List history = board.getMoveHistory(); + dos.writeInt(history.size()); + + for (RecordedMove move : history) { + dos.writeByte(move.row()); + dos.writeByte(move.col()); + dos.writeChar(move.player()); + } + } + } + + public GameRecord readGame(File file) throws IOException { + if (!file.exists()) { + throw new IOException("File not found"); + } + + try (DataInputStream dis = new DataInputStream( + new BufferedInputStream(new FileInputStream(file)))) { + + String header = dis.readUTF(); + if (!HEADER_SIGNATURE.equals(header)) { + throw new IOException("Invalid file format"); + } + + String date = dis.readUTF(); + String p1Name = dis.readUTF(); + String p2Name = dis.readUTF(); + char winner = dis.readChar(); + + int moveCount = dis.readInt(); + List moves = new ArrayList<>(moveCount); + + for (int i = 0; i < moveCount; i++) { + int row = dis.readByte(); + int col = dis.readByte(); + char player = dis.readChar(); + moves.add(new RecordedMove(row, col, player)); + } + + return new GameRecord(date, p1Name, p2Name, winner, moves); + } + } + + public List getAllRecordings() { + File directory = new File(DIRECTORY_PATH); + if (!directory.exists() || !directory.isDirectory()) { + return new ArrayList<>(); + } + File[] files = directory.listFiles((dir, name) -> name.endsWith(".dat")); + return files != null ? List.of(files) : new ArrayList<>(); + } +} \ No newline at end of file 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..c0312ce 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 @@ -1,12 +1,16 @@ +/* + * 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; import com.boredxgames.tictactoeclient.domain.model.GameState; - +import com.boredxgames.tictactoeclient.domain.model.RecordedMove; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** - * * @author Tasneem */ public class GameBoard { @@ -21,6 +25,8 @@ public class GameBoard { private char currentPlayer; private GameState gameState; private int movesCount; + + private final List moveHistory; public GameBoard() { this(PLAYER_X); @@ -28,6 +34,7 @@ public GameBoard() { public GameBoard(char startingPlayer) { board = new char[BOARD_SIZE][BOARD_SIZE]; + moveHistory = new ArrayList<>(); initializeBoard(); currentPlayer = startingPlayer; gameState = GameState.IN_PROGRESS; @@ -48,6 +55,7 @@ public void resetGame() { public void resetGame(char startingPlayer) { initializeBoard(); + moveHistory.clear(); currentPlayer = startingPlayer; gameState = GameState.IN_PROGRESS; movesCount = 0; @@ -64,6 +72,9 @@ public boolean makeMove(int row, int col, char player) { board[row][col] = player; movesCount++; + + moveHistory.add(new RecordedMove(row, col, player)); + updateGameState(); if (gameState == GameState.IN_PROGRESS) { @@ -82,11 +93,9 @@ public boolean isValidMove(int row, int col) { if (gameState != GameState.IN_PROGRESS) { return false; } - if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return false; } - return board[row][col] == EMPTY; } @@ -109,88 +118,49 @@ public boolean isBoardFull() { } public boolean checkWin(char player) { - // rows for (int i = 0; i < BOARD_SIZE; i++) { - if (board[i][0] == player && board[i][1] == player && board[i][2] == player) { - return true; - } + if (board[i][0] == player && board[i][1] == player && board[i][2] == player) return true; } - - // col for (int j = 0; j < BOARD_SIZE; j++) { - if (board[0][j] == player && board[1][j] == player && board[2][j] == player) { - return true; - } - } - - // diagonal left -> right - if (board[0][0] == player && board[1][1] == player && board[2][2] == player) { - return true; - } - - // diagonal right -> left - if (board[0][2] == player && board[1][1] == player && board[2][0] == player) { - return true; + if (board[0][j] == player && board[1][j] == player && board[2][j] == player) return true; } + if (board[0][0] == player && board[1][1] == player && board[2][2] == player) return true; + if (board[0][2] == player && board[1][1] == player && board[2][0] == player) return true; return false; } - + public int[] getWinningLine() { - if (gameState == GameState.IN_PROGRESS || gameState == GameState.DRAW) { - return null; - } - + if (gameState == GameState.IN_PROGRESS || gameState == GameState.DRAW) return null; char winner = (gameState == GameState.X_WINS) ? PLAYER_X : PLAYER_O; - // row for (int i = 0; i < BOARD_SIZE; i++) { - if (board[i][0] == winner && board[i][1] == winner && board[i][2] == winner) { + if (board[i][0] == winner && board[i][1] == winner && board[i][2] == winner) return new int[]{i, 0, i, 1, i, 2}; - } } - - // col for (int j = 0; j < BOARD_SIZE; j++) { - if (board[0][j] == winner && board[1][j] == winner && board[2][j] == winner) { + if (board[0][j] == winner && board[1][j] == winner && board[2][j] == winner) return new int[]{0, j, 1, j, 2, j}; - } } - - // diagonal left -> right - if (board[0][0] == winner && board[1][1] == winner && board[2][2] == winner) { + if (board[0][0] == winner && board[1][1] == winner && board[2][2] == winner) return new int[]{0, 0, 1, 1, 2, 2}; - } - - // diagonal right -> left - if (board[0][2] == winner && board[1][1] == winner && board[2][0] == winner) { + if (board[0][2] == winner && board[1][1] == winner && board[2][0] == winner) return new int[]{0, 2, 1, 1, 2, 0}; - } - + return null; } public char getWinner() { - if (gameState == GameState.X_WINS) { - return PLAYER_X; - } else if (gameState == GameState.O_WINS) { - return PLAYER_O; - } + if (gameState == GameState.X_WINS) return PLAYER_X; + if (gameState == GameState.O_WINS) return PLAYER_O; return EMPTY; } - public char getCurrentPlayer() { - return currentPlayer; - } - - public GameState getGameState() { - return gameState; - } - + public char getCurrentPlayer() { return currentPlayer; } + public GameState getGameState() { return gameState; } + public char getCellValue(int row, int col) { - if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { - return EMPTY; - } + if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return EMPTY; return board[row][col]; } @@ -198,11 +168,13 @@ public List getAvailableMoves() { List availableMoves = new ArrayList<>(); for (int i = 0; i < BOARD_SIZE; i++) { for (int j = 0; j < BOARD_SIZE; j++) { - if (board[i][j] == EMPTY) { - availableMoves.add(new int[]{i, j}); - } + if (board[i][j] == EMPTY) availableMoves.add(new int[]{i, j}); } } return availableMoves; } -} + + public List getMoveHistory() { + return Collections.unmodifiableList(moveHistory); + } +} \ 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 fe84999..78590ff 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java @@ -1,20 +1,25 @@ 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.GameRecord; +import com.boredxgames.tictactoeclient.domain.model.GameState; import com.boredxgames.tictactoeclient.domain.model.Move; import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; -import com.boredxgames.tictactoeclient.domain.model.GameState; -import com.boredxgames.tictactoeclient.domain.services.AIService; import com.boredxgames.tictactoeclient.domain.services.game.GameService; import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVEAIService; import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVPService; +import com.boredxgames.tictactoeclient.domain.services.storage.GameRecordingService; +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; +import javafx.animation.KeyFrame; import javafx.animation.PauseTransition; -import javafx.application.Platform; +import javafx.animation.Timeline; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; @@ -30,65 +35,38 @@ 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; -/** - * @author Tasneem - */ public class GameController implements Initializable, NavigationParameterAware { public GridPane gameGrid; - @FXML - private Button backButton; - @FXML - private Button settingsButton; - @FXML - private HBox difficultyBadge; - @FXML - private Label difficultyLabel; - - @FXML - private VBox playerCard; - @FXML - private VBox opponentCard; - @FXML - private Label playerNameLabel; - @FXML - private Label opponentNameLabel; - @FXML - private Label playerScoreLabel; - @FXML - private Label opponentScoreLabel; - @FXML - private Label opponentTypeLabel; - - @FXML - private Button cell00, cell01, cell02; - @FXML - private Button cell10, cell11, cell12; - @FXML - private Button cell20, cell21, cell22; + @FXML private Button backButton; + @FXML private Button settingsButton; + @FXML private HBox difficultyBadge; + @FXML private Label difficultyLabel; + + @FXML private VBox playerCard; + @FXML private VBox opponentCard; + @FXML private Label playerNameLabel; + @FXML private Label opponentNameLabel; + @FXML private Label playerScoreLabel; + @FXML private Label opponentScoreLabel; + @FXML private Label opponentTypeLabel; + + @FXML private Button cell00, cell01, cell02; + @FXML private Button cell10, cell11, cell12; + @FXML private Button cell20, cell21, cell22; private Button[][] cells; - @FXML - private StackPane modalOverlay; - @FXML - private Text modalIcon; - @FXML - private Label modalTitle; - @FXML - private Label modalMessage; - @FXML - private MediaView victoryVideo; - @FXML - private Button playAgainButton; - @FXML - private Button changeDifficultyButton; - @FXML - private Button mainMenuButton; + @FXML private StackPane modalOverlay; + @FXML private Text modalIcon; + @FXML private Label modalTitle; + @FXML private Label modalMessage; + @FXML private MediaView victoryVideo; + @FXML private Button playAgainButton; + @FXML private Button changeDifficultyButton; + @FXML private Button mainMenuButton; + @FXML private Button saveGameButton; private GameBoard gameBoard; private GameMode gameMode; @@ -99,6 +77,8 @@ public class GameController implements Initializable, NavigationParameterAware { private String player2Name = "Player 2"; private GameService gameService; + private final GameRecordingService recordingService = new GameRecordingService(); + private GameRecord replayData; @Override public void initialize(URL url, ResourceBundle rb) { @@ -118,8 +98,8 @@ public void setNavigationParameter(Object parameter) { this.player1Name = params.player1(); this.player2Name = params.player2(); this.gameMode = params.mode(); + this.replayData = params.replayData(); } else { - // Default this.gameMode = GameMode.OFFLINE_PVP; this.player1Name = "Player 1"; this.player2Name = "Player 2"; @@ -127,8 +107,11 @@ public void setNavigationParameter(Object parameter) { applyPlayerInfo(); applyGameModeSettings(); - resetGame(); + + if (gameMode == GameMode.REPLAY && replayData != null) { + startReplay(); + } } private void applyPlayerInfo() { @@ -160,7 +143,18 @@ private void applyGameModeSettings() { difficultyBadge.setManaged(false); changeDifficultyButton.setVisible(false); changeDifficultyButton.setManaged(false); - // gameService = new OnlinePVPService(); // TODO: implement online pvp service + } + case REPLAY -> { + opponentTypeLabel.setText("REPLAY"); + difficultyBadge.setVisible(false); + difficultyBadge.setManaged(false); + changeDifficultyButton.setVisible(false); + changeDifficultyButton.setManaged(false); + if (saveGameButton != null) { + saveGameButton.setVisible(false); + saveGameButton.setManaged(false); + } + disableBoard(); } } } @@ -170,7 +164,6 @@ private void setupCellHandlers() { for (int col = 0; col < 3; col++) { final int r = row; final int c = col; - cells[row][col].setOnAction(e -> handleCellClick(r, c)); } } @@ -180,62 +173,67 @@ private void setupButtonHandlers() { playAgainButton.setOnAction(e -> resetGame()); mainMenuButton.setOnAction(e -> { - NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE_ALL); // TODO: change to mode selection screen - }); - - backButton.setOnAction(e -> { - NavigationManager.pop(); + NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE_ALL); }); - settingsButton.setOnAction(e -> { + backButton.setOnAction(e -> NavigationManager.pop()); + settingsButton.setOnAction(e -> NavigationManager.navigate(Screens.SETTINGS, NavigationAction.REPLACE_ALL)); + changeDifficultyButton.setOnAction(e -> {}); + + if (saveGameButton != null) { + saveGameButton.setOnAction(e -> handleSaveGame()); + } + } - NavigationManager.navigate(Screens.SETTINGS, NavigationAction.REPLACE_ALL); - }); + private void handleSaveGame() { + String p1 = player1Name.replaceAll("[^a-zA-Z0-9]", ""); + String p2 = player2Name.replaceAll("[^a-zA-Z0-9]", ""); + long timestamp = System.currentTimeMillis(); + + String filename = String.format("REC_%s_vs_%s_%d", p1, p2, timestamp); - changeDifficultyButton.setOnAction(e -> { - // TODO change difficulty - }); + try { + recordingService.saveGame( + gameBoard, + player1Name, + player2Name, + filename + ); + + saveGameButton.setText("Saved!"); + saveGameButton.setDisable(true); + } catch (Exception ex) { + ex.printStackTrace(); + } } private void handleCellClick(int row, int col) { - if (!gameBoard.isValidMove(row, col)) { - return; - } + if (!gameBoard.isValidMove(row, col)) return; + if (gameMode == GameMode.REPLAY) return; char currentPlayer = gameBoard.getCurrentPlayer(); switch (gameMode) { case OFFLINE_PVP -> { - // Player vs Player - gameBoard.makeMove(row, col, currentPlayer); updateCell(row, col, currentPlayer); - if (!checkGameEnd()) { - updateTurnIndicator(); } } - case OFFLINE_PVE -> { - // Player X moves gameBoard.makeMove(row, col, GameBoard.PLAYER_X); updateCell(row, col, GameBoard.PLAYER_X); - if (checkGameEnd()) { - return; - } + if (checkGameEnd()) return; - // AI O moves disableBoard(); - PauseTransition pause = new PauseTransition(Duration.millis(500)); // AI "thinking" + PauseTransition pause = new PauseTransition(Duration.millis(500)); 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(); @@ -244,13 +242,27 @@ private void handleCellClick(int row, int col) { }); pause.play(); } + } + } - case ONLINE_PVP -> { - // Player sends move to server instead of executing locally - - // sendMoveToServer(row, col); - } + 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(); } private boolean checkGameEnd() { @@ -264,16 +276,7 @@ private boolean checkGameEnd() { private void updateCell(int row, int col, char player) { Button cell = cells[row][col]; - - Text symbol = new Text(); - symbol.getStyleClass().add("material-icon"); - - String iconPath; - if (player == GameBoard.PLAYER_X) { - iconPath = "/assets/icons/close.png"; - } else { - iconPath = "/assets/icons/circle.png"; - } + String iconPath = (player == GameBoard.PLAYER_X) ? "/assets/icons/close.png" : "/assets/icons/circle.png"; Image img = new Image(Objects.requireNonNull(getClass().getResourceAsStream(iconPath))); ImageView imgView = new ImageView(img); @@ -290,24 +293,12 @@ private void updateTurnIndicator() { char current = gameBoard.getCurrentPlayer(); boolean isPlayerX = (current == GameBoard.PLAYER_X); - if (gameMode == GameMode.OFFLINE_PVP) { + if (gameMode == GameMode.OFFLINE_PVP || gameMode == GameMode.OFFLINE_PVE || gameMode == GameMode.REPLAY) { if (isPlayerX) { setActiveCard(playerCard, opponentCard); } else { setActiveCard(opponentCard, playerCard); } - } else if (gameMode == GameMode.OFFLINE_PVE) { - if (isPlayerX) { - setActiveCard(playerCard, opponentCard); - } else { - setActiveCard(opponentCard, playerCard); - } - } else { - if (isPlayerTurn) { - setActiveCard(playerCard, opponentCard); - } else { - setActiveCard(opponentCard, playerCard); - } } } @@ -316,32 +307,6 @@ private void setActiveCard(VBox activeCard, VBox inactiveCard) { inactiveCard.getStyleClass().remove("active-card"); } - private void makeCPUMove() { - if (gameMode != GameMode.OFFLINE_PVE) { - return; - } - - int[] aiMove = AIService.nextMove(gameBoard, AIService.getDiffiulty()); - if (aiMove == null) { - return; - } - - Platform.runLater(() -> { - if (gameBoard.makeMove(aiMove[0], aiMove[1], GameBoard.PLAYER_O)) { - updateCell(aiMove[0], aiMove[1], GameBoard.PLAYER_O); - - GameState state = gameBoard.getGameState(); - if (state != GameState.IN_PROGRESS) { - handleGameEnd(); - } else { - gameBoard.switchPlayer(); - updateTurnIndicator(); - enableBoard(); - } - } - }); - } - private void handleGameEnd() { GameState state = gameBoard.getGameState(); @@ -355,12 +320,16 @@ private void handleGameEnd() { } if (state == GameState.X_WINS) { - playerScore++; - playerScoreLabel.setText(String.valueOf(playerScore)); + if (gameMode != GameMode.REPLAY) { + playerScore++; + playerScoreLabel.setText(String.valueOf(playerScore)); + } playVictoryVideo(); } else if (state == GameState.O_WINS) { - opponentScore++; - opponentScoreLabel.setText(String.valueOf(opponentScore)); + if (gameMode != GameMode.REPLAY) { + opponentScore++; + opponentScoreLabel.setText(String.valueOf(opponentScore)); + } } PauseTransition pause = new PauseTransition(Duration.millis(800)); @@ -377,7 +346,6 @@ private void showGameOverModal(GameState state) { ? "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!"); @@ -385,7 +353,6 @@ private void showGameOverModal(GameState state) { ? "The CPU outsmarted you this time." : "Player 2 wins the game!"); break; - case DRAW: modalIcon.setText("handshake"); modalTitle.setText("Draw!"); @@ -393,26 +360,35 @@ private void showGameOverModal(GameState state) { break; } + 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); + } else { + playAgainButton.setVisible(true); + playAgainButton.setManaged(true); + } + modalOverlay.setVisible(true); } private void playVictoryVideo() { try { - String videoPath = Objects.requireNonNull( - getClass().getResource("/assets/videos/you_win.mp4") - ).toExternalForm(); - + String videoPath = Objects.requireNonNull(getClass().getResource("/assets/videos/you_win.mp4")).toExternalForm(); Media media = new Media(videoPath); MediaPlayer player = new MediaPlayer(media); victoryVideo.setMediaPlayer(player); - victoryVideo.setVisible(true); player.setAutoPlay(true); - - player.setOnEndOfMedia(() -> { - victoryVideo.setVisible(false); - }); - + player.setOnEndOfMedia(() -> victoryVideo.setVisible(false)); } catch (Exception e) { System.out.println("Error getting the video: " + e); } @@ -420,7 +396,6 @@ private void playVictoryVideo() { public void resetGame() { gameBoard.resetGame(); - for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { cells[row][col].setGraphic(null); @@ -428,7 +403,6 @@ public void resetGame() { cells[row][col].getStyleClass().remove("cell-winning"); } } - modalOverlay.setVisible(false); updateTurnIndicator(); enableBoard(); @@ -437,9 +411,7 @@ public void resetGame() { private void disableBoard() { for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { - if (gameBoard.getCellValue(row, col) == GameBoard.EMPTY) { - cells[row][col].setDisable(true); - } + if (gameBoard.getCellValue(row, col) == GameBoard.EMPTY) cells[row][col].setDisable(true); } } } @@ -447,10 +419,8 @@ private void disableBoard() { private void enableBoard() { for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { - if (gameBoard.getCellValue(row, col) == GameBoard.EMPTY) { - cells[row][col].setDisable(false); - } + if (gameBoard.getCellValue(row, col) == GameBoard.EMPTY) cells[row][col].setDisable(false); } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java index 8521247..8df99d2 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java @@ -14,34 +14,20 @@ public class GameModeScreenController { - @FXML - private VBox offlineCard; - @FXML - private VBox onlineCard; - @FXML - private Button settingsBtn; - @FXML - private FlowPane cardsPane; - @FXML - private Label logoLabel; - @FXML - private Label titleLabel; - @FXML - private Label subtitleLabel; - @FXML - private Label offlineTitleLabel; - @FXML - private Label offlineDescLabel; - @FXML - private Label offlineActionLabel; - @FXML - private Label onlineTitleLabel; - @FXML - private Label onlineDescLabel; - @FXML - private Label onlineActionLabel; - @FXML - private Label footerLabel; + @FXML private VBox offlineCard; + @FXML private VBox onlineCard; + @FXML private Button settingsBtn; + @FXML private FlowPane cardsPane; + @FXML private Label logoLabel; + @FXML private Label titleLabel; + @FXML private Label subtitleLabel; + @FXML private Label offlineTitleLabel; + @FXML private Label offlineDescLabel; + @FXML private Label offlineActionLabel; + @FXML private Label onlineTitleLabel; + @FXML private Label onlineDescLabel; + @FXML private Label onlineActionLabel; + @FXML private Label footerLabel; public void initialize() { updateTexts(); @@ -49,25 +35,24 @@ public void initialize() { } private void setupActions() { - - offlineCard.setOnMouseClicked(e -> { - System.out.println("Offline Mode Selected"); NavigationManager.navigate(Screens.PVP_SETUP, NavigationAction.REPLACE); }); onlineCard.setOnMouseClicked(e -> { - System.out.println("Online Mode Selected"); NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE_ALL); }); - } @FXML private void openSettings(ActionEvent event) { - System.out.println("Settings Button Clicked"); NavigationManager.navigate(Screens.SETTINGS, NavigationAction.PUSH); } + + @FXML + private void openHistory(ActionEvent event) { + NavigationManager.navigate(Screens.RECORDINGS, NavigationAction.PUSH); + } private String safeGet(ResourceBundle bundle, String key, String fallback) { try { @@ -114,4 +99,4 @@ public void updateTexts() { logoLabel.setText(safeGet(bundle, "logo.text", "Tic-Tac-Toe")); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordingsListController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordingsListController.java new file mode 100644 index 0000000..652e498 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordingsListController.java @@ -0,0 +1,105 @@ +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.GameMode; +import com.boredxgames.tictactoeclient.domain.model.GameNavigationParams; +import com.boredxgames.tictactoeclient.domain.model.GameRecord; +import com.boredxgames.tictactoeclient.domain.services.storage.GameRecordingService; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.ResourceBundle; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +public class RecordingsListController implements Initializable { + + @FXML private VBox listContainer; + private final GameRecordingService recordingService = new GameRecordingService(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + loadRecordings(); + } + + private void loadRecordings() { + listContainer.getChildren().clear(); + List files = recordingService.getAllRecordings(); + + if (files.isEmpty()) { + Label empty = new Label("No recordings found."); + empty.setStyle("-fx-text-fill: #888; -fx-font-size: 16px;"); + listContainer.getChildren().add(empty); + return; + } + + for (File file : files) { + try { + GameRecord record = recordingService.readGame(file); + listContainer.getChildren().add(createCard(file, record)); + } catch (IOException e) { + System.out.println("Corrupt file: " + file.getName()); + } + } + } + + private HBox createCard(File file, GameRecord record) { + HBox card = new HBox(15); + card.setAlignment(Pos.CENTER_LEFT); + card.setStyle("-fx-background-color: rgba(255,255,255,0.05); -fx-background-radius: 8; -fx-padding: 15;"); + + Label icon = new Label("📼"); + icon.setStyle("-fx-font-size: 20px;"); + + VBox info = new VBox(4); + Label title = new Label(record.player1() + " vs " + record.player2()); + title.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 14px;"); + + Label date = new Label(record.date().substring(0, 10) + " • " + record.getResultDescription()); + date.setStyle("-fx-text-fill: #aaa; -fx-font-size: 11px;"); + + info.getChildren().addAll(title, date); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Button watchBtn = new Button("Watch"); + watchBtn.setStyle("-fx-background-color: #4f5ef7; -fx-text-fill: white; -fx-cursor: hand;"); + watchBtn.setOnAction(e -> watchReplay(record)); + + Button deleteBtn = new Button("🗑"); + deleteBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #ff4444; -fx-cursor: hand;"); + deleteBtn.setOnAction(e -> { + file.delete(); + loadRecordings(); + }); + + card.getChildren().addAll(icon, info, spacer, watchBtn, deleteBtn); + return card; + } + + private void watchReplay(GameRecord record) { + GameNavigationParams params = new GameNavigationParams( + record.player1(), + record.player2(), + GameMode.REPLAY, + record + ); + NavigationManager.navigate(Screens.GAME, NavigationAction.PUSH, params); + } + + @FXML + private void onBackClicked() { + NavigationManager.pop(); + } +} \ No newline at end of file diff --git a/src/main/resources/fxml/GameModeScreen.fxml b/src/main/resources/fxml/GameModeScreen.fxml index 59b3147..93b865e 100644 --- a/src/main/resources/fxml/GameModeScreen.fxml +++ b/src/main/resources/fxml/GameModeScreen.fxml @@ -15,10 +15,8 @@ xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.boredxgames.tictactoeclient.presentation.GameModeScreenController"> - - @@ -29,7 +27,6 @@ - @@ -43,36 +40,41 @@ - - + - + + + + +