From 0c248e30b680d392ee7c73c45a6a8d74b530d974 Mon Sep 17 00:00:00 2001 From: Sherif Ashraf Date: Sun, 11 Jan 2026 17:45:24 +0200 Subject: [PATCH 1/7] feat(ai): add AI package and Minimax support --- .../domain/services/ai/AIDifficulty.java | 15 +++++++++++ .../domain/services/ai/BoardUtils.java | 27 +++++++++++++++++++ .../domain/services/ai/MinimaxEngine.java | 13 +++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java new file mode 100644 index 0000000..7458c40 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java @@ -0,0 +1,15 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Enum.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.services.ai; + +/** + * + * @author sheri + */ +public enum AIDifficulty { + EASY, + MEDIUM, + HARD +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java new file mode 100644 index 0000000..72d7923 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java @@ -0,0 +1,27 @@ +/* + * 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.ai; + +import com.boredxgames.tictactoeclient.domain.services.GameBoard; + +/** + * + * @author sheri + */ +public class BoardUtils { + public static GameBoard copy(GameBoard original) { + GameBoard copy = new GameBoard(original.getCurrentPlayer()); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + char cell = original.getCellValue(i, j); + if (cell != GameBoard.EMPTY) { + copy.makeMove(i, j, cell); + } + } + } + return copy; + } +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java new file mode 100644 index 0000000..567c42e --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java @@ -0,0 +1,13 @@ +/* + * 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.ai; + +/** + * + * @author sheri + */ +public class MinimaxEngine { + +} From f6486fab65c7e80042164a1b2e6f894e11e516e3 Mon Sep 17 00:00:00 2001 From: Sherif Ashraf Date: Sun, 11 Jan 2026 18:36:15 +0200 Subject: [PATCH 2/7] feat(ai): add AIService for handling EASY, MEDIUM, HARD moves --- .../domain/services/ai/AIService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java new file mode 100644 index 0000000..a792173 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java @@ -0,0 +1,13 @@ +/* + * 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.ai; + +/** + * + * @author sheri + */ +public class AIService { + +} From f02f36122ca9c1198cd4236d5eb68ad545540afd Mon Sep 17 00:00:00 2001 From: Sherif Ashraf Date: Sun, 11 Jan 2026 22:08:52 +0200 Subject: [PATCH 3/7] update(ai): updated AIDifficulty, AIService, BoardUtils, GameBoard, and MinimaxEngine --- .../services/{ai => }/AIDifficulty.java | 2 +- .../domain/services/AIService.java | 48 +++++ .../domain/services/{ai => }/BoardUtils.java | 6 +- .../domain/services/GameBoard.java | 197 +++++------------- .../domain/services/MinimaxEngine.java | 52 +++++ .../domain/services/ai/AIService.java | 13 -- .../domain/services/ai/MinimaxEngine.java | 13 -- 7 files changed, 158 insertions(+), 173 deletions(-) rename src/main/java/com/boredxgames/tictactoeclient/domain/services/{ai => }/AIDifficulty.java (82%) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java rename src/main/java/com/boredxgames/tictactoeclient/domain/services/{ai => }/BoardUtils.java (79%) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java delete mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java delete mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIDifficulty.java similarity index 82% rename from src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java rename to src/main/java/com/boredxgames/tictactoeclient/domain/services/AIDifficulty.java index 7458c40..cc17852 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIDifficulty.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIDifficulty.java @@ -2,7 +2,7 @@ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Enum.java to edit this template */ -package com.boredxgames.tictactoeclient.domain.services.ai; +package com.boredxgames.tictactoeclient.domain.services; /** * diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java new file mode 100644 index 0000000..756653d --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java @@ -0,0 +1,48 @@ +/* + * 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; + +import com.boredxgames.tictactoeclient.domain.services.GameBoard; +import java.util.List; +import java.util.Random; + +/** + * + * @author sheri + */ +public class AIService { + + private static final Random random = new Random(); + + public static int[] nextMove(GameBoard board, AIDifficulty difficulty) { + return switch (difficulty) { + case EASY -> + randomMove(board); + case MEDIUM -> + mediumMove(board); + case HARD -> + hardMove(board); + }; + } + + // EASY + private static int[] randomMove(GameBoard board) { + List moves = board.getAvailableMoves(); + return moves.get(random.nextInt(moves.size())); + } + + // MEDIUM + private static int[] mediumMove(GameBoard board) { + if (random.nextBoolean()) { + return hardMove(board); + } + return randomMove(board); + } + + // HARD + private static int[] hardMove(GameBoard board) { + return MinimaxEngine.bestMove(board); + } +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java similarity index 79% rename from src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java rename to src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java index 72d7923..ce2bc2c 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/BoardUtils.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.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.ai; +package com.boredxgames.tictactoeclient.domain.services; import com.boredxgames.tictactoeclient.domain.services.GameBoard; @@ -11,14 +11,14 @@ * @author sheri */ public class BoardUtils { - public static GameBoard copy(GameBoard original) { +public static GameBoard copy(GameBoard original) { GameBoard copy = new GameBoard(original.getCurrentPlayer()); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { char cell = original.getCellValue(i, j); if (cell != GameBoard.EMPTY) { - copy.makeMove(i, j, cell); + copy.forceMove(i, j, cell); } } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java index e344a9b..51ec67f 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java @@ -8,198 +8,109 @@ * @author Tasneem */ public class GameBoard { - private final char[][] board; - private final int BOARD_SIZE = 3; + private final char[][] board; + private static final int BOARD_SIZE = 3; public static final char PLAYER_X = 'X'; public static final char PLAYER_O = 'O'; public static final char EMPTY = '-'; - + private char currentPlayer; private GameState gameState; private int movesCount; - + public enum GameState { IN_PROGRESS, X_WINS, O_WINS, DRAW } - + public GameBoard() { this(PLAYER_X); } - + public GameBoard(char startingPlayer) { board = new char[BOARD_SIZE][BOARD_SIZE]; - initializeBoard(); - currentPlayer = startingPlayer; - gameState = GameState.IN_PROGRESS; - movesCount = 0; + resetGame(startingPlayer); } - - private void initializeBoard() { + + public void resetGame(char startingPlayer) { for (int i = 0; i < BOARD_SIZE; i++) { for (int j = 0; j < BOARD_SIZE; j++) { board[i][j] = EMPTY; } } - } - - public void resetGame() { - resetGame(PLAYER_X); - } - - public void resetGame(char startingPlayer) { - initializeBoard(); currentPlayer = startingPlayer; gameState = GameState.IN_PROGRESS; movesCount = 0; } - + + // public boolean makeMove(int row, int col) { - return makeMove(row, col, currentPlayer); - } - - public boolean makeMove(int row, int col, char player) { - if (!isValidMove(row, col)) { - return false; - } - - board[row][col] = player; + if (!isValidMove(row, col)) return false; + + board[row][col] = currentPlayer; movesCount++; - - currentPlayer = player; + updateGameState(); + + if (gameState == GameState.IN_PROGRESS) { + switchPlayer(); + } return true; } + + void forceMove(int row, int col, char player) { + board[row][col] = player; + movesCount++; + } + 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; - } - + if (gameState != GameState.IN_PROGRESS) return false; + if (row < 0 || col < 0 || row >= BOARD_SIZE || col >= BOARD_SIZE) return false; return board[row][col] == EMPTY; } - - + public void switchPlayer() { currentPlayer = (currentPlayer == PLAYER_X) ? PLAYER_O : PLAYER_X; } - + private void updateGameState() { - if (checkWin(PLAYER_X)) { - gameState = GameState.X_WINS; - } else if (checkWin(PLAYER_O)) { - gameState = GameState.O_WINS; - } else if (isBoardFull()) { - gameState = GameState.DRAW; - } - } - - public boolean isBoardFull() { - return movesCount == BOARD_SIZE * BOARD_SIZE; + if (checkWin(PLAYER_X)) gameState = GameState.X_WINS; + else if (checkWin(PLAYER_O)) gameState = GameState.O_WINS; + else if (movesCount == BOARD_SIZE * BOARD_SIZE) gameState = GameState.DRAW; } - + 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) { + for (int i = 0; i < BOARD_SIZE; i++) + 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) { + + 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) { + + 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) { + + 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; - } - - 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) { - 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) { - 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) { - 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) { - 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; - } - return EMPTY; - } - - 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; - } - return board[row][col]; - } - + 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}); - } - } - } - return availableMoves; + List moves = new ArrayList<>(); + for (int i = 0; i < BOARD_SIZE; i++) + for (int j = 0; j < BOARD_SIZE; j++) + if (board[i][j] == EMPTY) + moves.add(new int[]{i, j}); + return moves; } + + public char getCellValue(int r, int c) { return board[r][c]; } + public char getCurrentPlayer() { return currentPlayer; } + public GameState getGameState() { return gameState; } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java new file mode 100644 index 0000000..daa65d0 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java @@ -0,0 +1,52 @@ +/* + * 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; + +import com.boredxgames.tictactoeclient.domain.services.GameBoard; + +/** + * + * @author sheri + */ +public class MinimaxEngine { + +private static final char AI = GameBoard.PLAYER_O; + private static final char HUMAN = GameBoard.PLAYER_X; + + public static int[] bestMove(GameBoard board) { + int bestScore = Integer.MIN_VALUE; + int[] bestMove = null; + + for (int[] move : board.getAvailableMoves()) { + GameBoard copy = BoardUtils.copy(board); + copy.forceMove(move[0], move[1], AI); + + int score = minimax(copy, false); + if (score > bestScore) { + bestScore = score; + bestMove = move; + } + } + return bestMove; + } + + private static int minimax(GameBoard board, boolean isMax) { + + if (board.checkWin(AI)) return 10; + if (board.checkWin(HUMAN)) return -10; + if (board.getAvailableMoves().isEmpty()) return 0; + + int best = isMax ? Integer.MIN_VALUE : Integer.MAX_VALUE; + + for (int[] move : board.getAvailableMoves()) { + GameBoard copy = BoardUtils.copy(board); + copy.forceMove(move[0], move[1], isMax ? AI : HUMAN); + + int score = minimax(copy, !isMax); + best = isMax ? Math.max(best, score) : Math.min(best, score); + } + return best; + } +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java deleted file mode 100644 index a792173..0000000 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/AIService.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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.ai; - -/** - * - * @author sheri - */ -public class AIService { - -} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java deleted file mode 100644 index 567c42e..0000000 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/ai/MinimaxEngine.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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.ai; - -/** - * - * @author sheri - */ -public class MinimaxEngine { - -} From 81601fe30015c7098c9fbf561261b5a5183963c4 Mon Sep 17 00:00:00 2001 From: Sherif Ashraf Date: Mon, 12 Jan 2026 22:59:09 +0200 Subject: [PATCH 4/7] Draft: Added AI modules (AIDifficulty, AIServices, BoardUtils, MinimaxEngine) --- .../domain/services/AIService.java | 24 +++----- .../domain/services/BoardUtils.java | 12 ++-- .../domain/services/GameBoard.java | 58 ++++++++++--------- .../domain/services/MinimaxEngine.java | 34 ++++++----- 4 files changed, 62 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java index 756653d..1ef5e69 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java @@ -4,7 +4,6 @@ */ package com.boredxgames.tictactoeclient.domain.services; -import com.boredxgames.tictactoeclient.domain.services.GameBoard; import java.util.List; import java.util.Random; @@ -13,36 +12,29 @@ * @author sheri */ public class AIService { - - private static final Random random = new Random(); + private static final Random random = new Random(); public static int[] nextMove(GameBoard board, AIDifficulty difficulty) { + if (board.getAvailableMoves().isEmpty()) return null; + return switch (difficulty) { - case EASY -> - randomMove(board); - case MEDIUM -> - mediumMove(board); - case HARD -> - hardMove(board); + case EASY -> randomMove(board); + case MEDIUM -> mediumMove(board); + case HARD -> hardMove(board); }; } - // EASY private static int[] randomMove(GameBoard board) { List moves = board.getAvailableMoves(); return moves.get(random.nextInt(moves.size())); } - // MEDIUM private static int[] mediumMove(GameBoard board) { - if (random.nextBoolean()) { - return hardMove(board); - } + if (random.nextBoolean()) return hardMove(board); return randomMove(board); } - // HARD private static int[] hardMove(GameBoard board) { - return MinimaxEngine.bestMove(board); + return MinimaxEngine.bestMove(board, board.getCurrentPlayer()); } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java index ce2bc2c..803afcb 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java @@ -4,24 +4,20 @@ */ package com.boredxgames.tictactoeclient.domain.services; -import com.boredxgames.tictactoeclient.domain.services.GameBoard; + /** * * @author sheri */ public class BoardUtils { -public static GameBoard copy(GameBoard original) { + public static GameBoard copy(GameBoard original) { GameBoard copy = new GameBoard(original.getCurrentPlayer()); - - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) { char cell = original.getCellValue(i, j); - if (cell != GameBoard.EMPTY) { - copy.forceMove(i, j, cell); - } + if (cell != GameBoard.EMPTY) copy.forceMove(i, j, cell); } - } return copy; } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java index 51ec67f..ade37f1 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameBoard.java @@ -1,3 +1,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; import java.util.ArrayList; @@ -5,10 +9,10 @@ /** * - * @author Tasneem + * @author sheri */ public class GameBoard { - private final char[][] board; + private final char[][] board; private static final int BOARD_SIZE = 3; public static final char PLAYER_X = 'X'; @@ -36,32 +40,26 @@ public GameBoard(char startingPlayer) { } public void resetGame(char startingPlayer) { - for (int i = 0; i < BOARD_SIZE; i++) { - for (int j = 0; j < BOARD_SIZE; j++) { + for (int i = 0; i < BOARD_SIZE; i++) + for (int j = 0; j < BOARD_SIZE; j++) board[i][j] = EMPTY; - } - } + currentPlayer = startingPlayer; gameState = GameState.IN_PROGRESS; movesCount = 0; } - // public boolean makeMove(int row, int col) { if (!isValidMove(row, col)) return false; board[row][col] = currentPlayer; movesCount++; - updateGameState(); - if (gameState == GameState.IN_PROGRESS) { - switchPlayer(); - } + if (gameState == GameState.IN_PROGRESS) switchPlayer(); return true; } - void forceMove(int row, int col, char player) { board[row][col] = player; movesCount++; @@ -85,18 +83,13 @@ private void updateGameState() { public boolean checkWin(char player) { 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; for (int j = 0; j < BOARD_SIZE; j++) - 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][j] == player && board[1][j] == player && board[2][j] == player) return true; - if (board[0][2] == player && board[1][1] == player && board[2][0] == 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; } @@ -105,12 +98,25 @@ public List getAvailableMoves() { List moves = new ArrayList<>(); for (int i = 0; i < BOARD_SIZE; i++) for (int j = 0; j < BOARD_SIZE; j++) - if (board[i][j] == EMPTY) - moves.add(new int[]{i, j}); + if (board[i][j] == EMPTY) moves.add(new int[]{i, j}); return moves; } - public char getCellValue(int r, int c) { return board[r][c]; } - public char getCurrentPlayer() { return currentPlayer; } - public GameState getGameState() { return gameState; } + public char getWinner() { + if (gameState == GameState.X_WINS) return PLAYER_X; + else if (gameState == GameState.O_WINS) return PLAYER_O; + return EMPTY; + } + + public char getCellValue(int r, int c) { + return board[r][c]; + } + + public char getCurrentPlayer() { + return currentPlayer; + } + + public GameState getGameState() { + return gameState; + } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java index daa65d0..f0ad519 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java @@ -4,26 +4,23 @@ */ package com.boredxgames.tictactoeclient.domain.services; -import com.boredxgames.tictactoeclient.domain.services.GameBoard; - /** * * @author sheri */ public class MinimaxEngine { -private static final char AI = GameBoard.PLAYER_O; - private static final char HUMAN = GameBoard.PLAYER_X; - - public static int[] bestMove(GameBoard board) { + public static int[] bestMove(GameBoard board, char player) { int bestScore = Integer.MIN_VALUE; int[] bestMove = null; + char opponent = (player == GameBoard.PLAYER_X) ? GameBoard.PLAYER_O : GameBoard.PLAYER_X; + for (int[] move : board.getAvailableMoves()) { GameBoard copy = BoardUtils.copy(board); - copy.forceMove(move[0], move[1], AI); + copy.forceMove(move[0], move[1], player); - int score = minimax(copy, false); + int score = minimax(copy, false, 0, player, opponent); if (score > bestScore) { bestScore = score; bestMove = move; @@ -32,21 +29,26 @@ public static int[] bestMove(GameBoard board) { return bestMove; } - private static int minimax(GameBoard board, boolean isMax) { - - if (board.checkWin(AI)) return 10; - if (board.checkWin(HUMAN)) return -10; - if (board.getAvailableMoves().isEmpty()) return 0; + private static int minimax(GameBoard board, boolean isMax, int depth, char player, char opponent) { + if (board.checkWin(player)) { + return 10 - depth; + } + if (board.checkWin(opponent)) { + return depth - 10; + } + if (board.getAvailableMoves().isEmpty()) { + return 0; + } int best = isMax ? Integer.MIN_VALUE : Integer.MAX_VALUE; for (int[] move : board.getAvailableMoves()) { GameBoard copy = BoardUtils.copy(board); - copy.forceMove(move[0], move[1], isMax ? AI : HUMAN); - - int score = minimax(copy, !isMax); + copy.forceMove(move[0], move[1], isMax ? player : opponent); + int score = minimax(copy, !isMax, depth + 1, player, opponent); best = isMax ? Math.max(best, score) : Math.min(best, score); } + return best; } } From 04da1a4a4d9364834eab1f9d9fdf339faf9de285 Mon Sep 17 00:00:00 2001 From: Sherif Ashraf Date: Tue, 13 Jan 2026 13:47:32 +0200 Subject: [PATCH 5/7] Implemented offline AI gameplay with Easy/Medium/Hard difficulties --- .../navigation/NavigationManager.java | 4 +- .../tictactoeclient/domain/model/Move.java | 12 +- .../domain/services/AIService.java | 15 +- .../domain/services/BoardUtils.java | 3 +- .../domain/services/MinimaxEngine.java | 2 + .../domain/services/game/GameBoard.java | 84 ++++++----- .../services/game/OfflinePVEAIService.java | 41 ++++++ .../Difficulty_selectionController.java | 63 +++++++- .../presentation/GameController.java | 134 ++++++++++-------- .../GameModeScreenController.java | 4 +- .../resources/fxml/difficulty_selection.fxml | 8 +- 11 files changed, 254 insertions(+), 116 deletions(-) create mode 100644 src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java 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 c447254..53b9c92 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,13 +20,13 @@ private NavigationManager() { private static final Stack screenStack = new Stack<>(); public static Scene init() throws IOException { -// شاشة البداية + current = new ScreenNavigationEntry(Screens.PRIMARY, null, null); Parent root = initRoot(current.screen().getName()); scene = new Scene(root, 640, 480); - // طبق الثيم على الـ Scene الرئيسي وفعل listener لأي تغيير مستقبلي + ThemeManager.init(scene); return scene; diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Move.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/Move.java index 3b84e6e..cae1af2 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Move.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/Move.java @@ -1,14 +1,20 @@ package com.boredxgames.tictactoeclient.domain.model; + +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; + /** * @author Tasneem */ public class Move { + private final int row; private final int col; + private final GameBoard board; - public Move(int row, int col) { + public Move(int row, int col, GameBoard board) { this.row = row; this.col = col; + this.board = board; } public int getRow() { @@ -18,4 +24,8 @@ public int getRow() { public int getCol() { return col; } + + public GameBoard getBoard() { + return board; + } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java index 1ef5e69..3c8c49a 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/AIService.java @@ -4,6 +4,8 @@ */ package com.boredxgames.tictactoeclient.domain.services; +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; + import java.util.List; import java.util.Random; @@ -13,7 +15,8 @@ */ public class AIService { private static final Random random = new Random(); - + private static AIDifficulty diffiulty = AIDifficulty.EASY; + public static int[] nextMove(GameBoard board, AIDifficulty difficulty) { if (board.getAvailableMoves().isEmpty()) return null; @@ -24,6 +27,16 @@ public static int[] nextMove(GameBoard board, AIDifficulty difficulty) { }; } + public static AIDifficulty getDiffiulty() { + return diffiulty; + } + + public static void setDiffiulty(AIDifficulty diffiulty) { + AIService.diffiulty = diffiulty; + } + + + private static int[] randomMove(GameBoard board) { List moves = board.getAvailableMoves(); return moves.get(random.nextInt(moves.size())); diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java index 803afcb..ecb3f9c 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/BoardUtils.java @@ -5,13 +5,14 @@ package com.boredxgames.tictactoeclient.domain.services; +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; /** * * @author sheri */ public class BoardUtils { - public static GameBoard copy(GameBoard original) { + public static GameBoard copy(GameBoard original) { GameBoard copy = new GameBoard(original.getCurrentPlayer()); for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) { diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java index f0ad519..ffbca61 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/MinimaxEngine.java @@ -4,6 +4,8 @@ */ package com.boredxgames.tictactoeclient.domain.services; +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; + /** * * @author sheri 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 d583126..900730b 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 @@ -10,21 +10,22 @@ * @author Tasneem */ public class GameBoard { + private final char[][] board; private final int BOARD_SIZE = 3; public static final char PLAYER_X = 'X'; public static final char PLAYER_O = 'O'; public static final char EMPTY = '-'; - + private char currentPlayer; private GameState gameState; private int movesCount; - + public GameBoard() { this(PLAYER_X); } - + public GameBoard(char startingPlayer) { board = new char[BOARD_SIZE][BOARD_SIZE]; initializeBoard(); @@ -32,7 +33,7 @@ public GameBoard(char startingPlayer) { gameState = GameState.IN_PROGRESS; movesCount = 0; } - + private void initializeBoard() { for (int i = 0; i < BOARD_SIZE; i++) { for (int j = 0; j < BOARD_SIZE; j++) { @@ -40,52 +41,59 @@ private void initializeBoard() { } } } - + public void resetGame() { resetGame(PLAYER_X); } - + public void resetGame(char startingPlayer) { initializeBoard(); currentPlayer = startingPlayer; gameState = GameState.IN_PROGRESS; movesCount = 0; } - + public boolean makeMove(int row, int col) { return makeMove(row, col, currentPlayer); } - + public boolean makeMove(int row, int col, char player) { if (!isValidMove(row, col)) { return false; - } - + } + board[row][col] = player; movesCount++; - - currentPlayer = player; updateGameState(); + + if (gameState == GameState.IN_PROGRESS) { + currentPlayer = (player == PLAYER_X) ? PLAYER_O : PLAYER_X; + } + return true; } - + + public void forceMove(int row, int col, char player) { + board[row][col] = player; + movesCount++; + } + public boolean isValidMove(int row, int col) { - if(gameState != GameState.IN_PROGRESS) { + if (gameState != GameState.IN_PROGRESS) { return false; } - - if(row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { + + if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return false; } - + return board[row][col] == EMPTY; } - - + public void switchPlayer() { currentPlayer = (currentPlayer == PLAYER_X) ? PLAYER_O : PLAYER_X; } - + private void updateGameState() { if (checkWin(PLAYER_X)) { gameState = GameState.X_WINS; @@ -95,11 +103,11 @@ private void updateGameState() { gameState = GameState.DRAW; } } - + public boolean isBoardFull() { return movesCount == BOARD_SIZE * BOARD_SIZE; } - + public boolean checkWin(char player) { // rows for (int i = 0; i < BOARD_SIZE; i++) { @@ -107,61 +115,61 @@ public boolean checkWin(char 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; } - + return false; } - + public int[] getWinningLine() { 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) { 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) { 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) { 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) { return new int[]{0, 2, 1, 1, 2, 0}; } - + return null; } - + public char getWinner() { if (gameState == GameState.X_WINS) { return PLAYER_X; @@ -170,22 +178,22 @@ public char getWinner() { } return EMPTY; } - + 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; } return board[row][col]; } - + public List getAvailableMoves() { List availableMoves = new ArrayList<>(); for (int i = 0; i < BOARD_SIZE; i++) { 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 new file mode 100644 index 0000000..6ab837f --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java @@ -0,0 +1,41 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Interface.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.Move; +import com.boredxgames.tictactoeclient.domain.services.AIService; + +/** + * + * @author sheri + */ +public class OfflinePVEAIService implements GameService { + + @Override + public void makeMove(Move move, char currentPlayer) { + if (move != null) { + GameBoard board = move.getBoard(); + board.makeMove(move.getRow(), move.getCol(), currentPlayer); + } + } + + @Override + public Move getNextMove(GameBoard board, char currentPlayer) { + if (currentPlayer == GameBoard.PLAYER_O) { + int[] aiMove = AIService.nextMove(board, AIService.getDiffiulty()); + if (aiMove != null) { + return new Move(aiMove[0], aiMove[1], board); + } + } + return null; + } + + @Override + public GameState getOutcome(GameBoard board) { + return board.getGameState(); + } + +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/Difficulty_selectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/Difficulty_selectionController.java index 9bb918f..5346a6e 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/Difficulty_selectionController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/Difficulty_selectionController.java @@ -4,13 +4,23 @@ */ 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.services.AIDifficulty; +import com.boredxgames.tictactoeclient.domain.services.AIService; import java.net.URL; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.layout.Pane; + /** * FXML Controller class * @@ -18,9 +28,17 @@ */ public class Difficulty_selectionController implements Initializable { - @FXML private Pane backgroundPane; + @FXML + private Button easyBtn; + @FXML + private Button mediumBtn; + @FXML + private Button hardBtn; + @FXML + private Label backLabel; + /** * Initializes the controller class. */ @@ -28,14 +46,45 @@ public class Difficulty_selectionController implements Initializable { public void initialize(URL url, ResourceBundle rb) { backgroundPane.getChildren().clear(); - Platform.runLater(() -> { BackgroundAnimation.startWarpAnimation( - backgroundPane, - backgroundPane.getWidth(), - backgroundPane.getHeight() + backgroundPane, + backgroundPane.getWidth(), + backgroundPane.getHeight() ); }); - } - + + easyBtn.setOnAction(e -> startGameWithDifficulty("EASY")); + mediumBtn.setOnAction(e -> startGameWithDifficulty("MEDIUM")); + hardBtn.setOnAction(e -> startGameWithDifficulty("HARD")); + + backLabel.setOnMouseClicked(e -> goBack()); + } + + private void startGameWithDifficulty(String difficulty) { + System.out.println("Starting game with difficulty: " + difficulty); + + switch (difficulty) { + case "MEDIUM": + AIService.setDiffiulty(AIDifficulty.MEDIUM); + break; + case "HARD": + AIService.setDiffiulty(AIDifficulty.HARD); + break; + default: + AIService.setDiffiulty(AIDifficulty.EASY); + } + GameNavigationParams params = new GameNavigationParams( + "Player", + "CPU", + GameMode.OFFLINE_PVE + ); + NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, params); + + } + + private void goBack() { + System.out.println("Back to previous screen"); + NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE); + } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java index 457b824..11be083 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java @@ -9,7 +9,9 @@ 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 javafx.animation.PauseTransition; import javafx.application.Platform; import javafx.fxml.FXML; @@ -100,9 +102,9 @@ 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(); @@ -111,12 +113,15 @@ public void initialize(URL url, ResourceBundle rb) { @Override public void setNavigationParameter(Object parameter) { - if (parameter instanceof GameNavigationParams(String player1, String player2, GameMode mode)) { - this.player1Name = player1; - this.player2Name = player2; - this.gameMode = mode; - } else if (parameter instanceof GameMode mode) { - this.gameMode = mode; + if (parameter instanceof GameNavigationParams params) { + this.player1Name = params.player1(); + this.player2Name = params.player2(); + this.gameMode = params.mode(); + } else { + // Default + this.gameMode = GameMode.OFFLINE_PVP; + this.player1Name = "Player 1"; + this.player2Name = "Player 2"; } applyPlayerInfo(); @@ -146,7 +151,7 @@ private void applyGameModeSettings() { difficultyBadge.setManaged(true); changeDifficultyButton.setVisible(true); changeDifficultyButton.setManaged(true); - // gameService = new OfflinePVEAIService(); // TODO: implement offline pve ai service + gameService = new OfflinePVEAIService(); } case ONLINE_PVP -> { opponentTypeLabel.setText("ONLINE PLAYER"); @@ -192,34 +197,41 @@ 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); - - Move nextMove = gameService.getNextMove(gameBoard, currentPlayer); - 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(); - } - } - }); + if (!gameBoard.isValidMove(row, col)) { + return; + } + + + gameBoard.makeMove(row, col, GameBoard.PLAYER_X); + updateCell(row, col, GameBoard.PLAYER_X); + + if (checkGameEnd()) { + return; + } + + + if (gameMode == GameMode.OFFLINE_PVE) { + disableBoard(); + Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O); + gameService.makeMove(aiMove, GameBoard.PLAYER_O); + + if (aiMove != null) { + updateCell(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O); + } + + if (!checkGameEnd()) { + enableBoard(); + } } + } - GameState state = gameService.getOutcome(gameBoard); + private boolean checkGameEnd() { + GameState state = gameBoard.getGameState(); if (state != GameState.IN_PROGRESS) { handleGameEnd(); + return true; } + return false; } private void updateCell(int row, int col, char player) { @@ -277,27 +289,29 @@ private void setActiveCard(VBox activeCard, VBox inactiveCard) { } private void makeCPUMove() { - var availableMoves = gameBoard.getAvailableMoves(); - - if (!availableMoves.isEmpty()) { - // TODO: change difficulty - int randomIndex = (int) (Math.random() * availableMoves.size()); - int[] move = availableMoves.get(randomIndex); - - Platform.runLater(() -> { - if (gameBoard.makeMove(move[0], move[1], GameBoard.PLAYER_O)) { - updateCell(move[0], move[1], GameBoard.PLAYER_O); - - if (gameBoard.getGameState() != GameState.IN_PROGRESS) { - handleGameEnd(); - } else { - gameBoard.switchPlayer(); - updateTurnIndicator(); - enableBoard(); - } - } - }); + 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() { @@ -331,17 +345,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: @@ -411,4 +425,4 @@ private void enableBoard() { } } } -} \ 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 b0e30e9..673d354 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java @@ -50,10 +50,10 @@ public void initialize() { private void setupActions() { - // عند الضغط على الكارد الخاص بالـ Offline + offlineCard.setOnMouseClicked(e -> { System.out.println("Offline Mode Selected"); - NavigationManager.navigate(Screens.PVP_SETUP, NavigationAction.REPLACE); + NavigationManager.navigate(Screens.DifficultySelection, NavigationAction.REPLACE); }); onlineCard.setOnMouseClicked(e -> { diff --git a/src/main/resources/fxml/difficulty_selection.fxml b/src/main/resources/fxml/difficulty_selection.fxml index 510cea6..8e657ec 100644 --- a/src/main/resources/fxml/difficulty_selection.fxml +++ b/src/main/resources/fxml/difficulty_selection.fxml @@ -54,7 +54,7 @@