/**
 * Représente la logique métier du jeu 2048.
 * Gère l'état du plateau de jeu, les déplacements des tuiles, les fusions,
 * le score de la partie en cours, ainsi que les conditions de victoire ou de défaite.
 * Cette classe est conçue pour être indépendante du framework Android, ne contenant
 * aucune dépendance au Contexte Android ou aux SharedPreferences.
 */
package legion.muyue.best2048;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable; // Ajout pour le retour de deserialize
import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Game {

    /** La taille du plateau de jeu (nombre de lignes/colonnes, typiquement 4x4). */
    private static final int BOARD_SIZE = 4;

    /** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
    private int[][] board;
    /** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
    private final Random randomNumberGenerator;
    /** Score de la partie actuellement en cours. */
    private int currentScore = 0;
    /** Meilleur score global connu par cette instance (généralement défini depuis l'extérieur). */
    private int highestScore = 0;
    /** Indicateur si la condition de victoire (une tuile >= 2048) a été atteinte. */
    private boolean gameWon = false;
    /** Indicateur si la partie est terminée (plus de mouvements ou de fusions possibles). */
    private boolean gameOver = false;

    /**
     * Constructeur pour démarrer une nouvelle partie.
     * Initialise un plateau vide, met le score courant à 0, réinitialise les états
     * de victoire/défaite, et ajoute deux tuiles initiales aléatoirement.
     */
    public Game() {
        this.randomNumberGenerator = new Random();
        initializeNewBoard();
    }

    /**
     * Constructeur pour restaurer une partie à partir d'un état sauvegardé.
     * Le meilleur score (`highestScore`) doit être défini séparément via {@link #setHighestScore(int)}.
     * Recalcule les états `gameWon` et `gameOver` en fonction du plateau et du score fournis.
     * Lance une exception si le plateau fourni n'a pas les bonnes dimensions.
     *
     * @param board Le plateau de jeu restauré. Doit être de dimensions BOARD_SIZE x BOARD_SIZE.
     * @param score Le score courant restauré.
     * @throws IllegalArgumentException si les dimensions du plateau fourni sont incorrectes.
     */
    public Game(@NonNull int[][] board, int score) {
        // Validation des dimensions du plateau
        if (board == null || board.length != BOARD_SIZE || board[0].length != BOARD_SIZE) {
            throw new IllegalArgumentException("Le plateau fourni n'a pas les dimensions attendues (" + BOARD_SIZE + "x" + BOARD_SIZE + ").");
        }
        this.board = board; // Attention: shallow copy si board est modifié ailleurs après coup. getBoard() est plus sûr.
        // Pour la restauration, on suppose que le tableau fourni est destiné à cet usage unique.
        this.currentScore = score;
        this.randomNumberGenerator = new Random();
        // Recalculer l'état de victoire/défaite basé sur le plateau chargé
        checkWinCondition();
        // Vérifier si la partie chargée est déjà terminée
        checkGameOverCondition(); // Important si on charge une partie déjà terminée
    }

    // --- Getters / Setters ---

    /**
     * Retourne la valeur de la tuile aux coordonnées spécifiées (ligne, colonne).
     * Les indices sont basés sur 0.
     *
     * @param row Ligne de la cellule (0 à BOARD_SIZE-1).
     * @param column Colonne de la cellule (0 à BOARD_SIZE-1).
     * @return La valeur de la tuile, ou 0 si la cellule est vide ou les coordonnées sont invalides.
     */
    public int getCellValue(int row, int column) {
        if (isIndexValid(row, column)) {
            return this.board[row][column];
        }
        // Log ou gestion d'erreur pourrait être ajouté si un accès invalide est critique
        return 0; // Retourne 0 pour indice invalide comme convention
    }

    /**
     * Définit la valeur d'une tuile aux coordonnées spécifiées.
     * Utilisé principalement en interne ou pour les tests. Ne fait rien si
     * les coordonnées (ligne, colonne) sont en dehors des limites du plateau.
     *
     * @param row Ligne de la cellule (0 à BOARD_SIZE-1).
     * @param col Colonne de la cellule (0 à BOARD_SIZE-1).
     * @param value Nouvelle valeur entière pour la tuile.
     */
    @VisibleForTesting // Indique que cette méthode est surtout pour les tests
    void setCellValue(int row, int col, int value) {
        if (isIndexValid(row, col)) {
            this.board[row][col] = value;
        }
    }

    /**
     * Retourne le score actuel de la partie en cours.
     * Le score augmente lors de la fusion de tuiles.
     *
     * @return Le score entier actuel.
     */
    public int getCurrentScore() {
        return currentScore;
    }

    /**
     * Retourne le meilleur score connu par cette instance de jeu.
     * Cette valeur est généralement chargée depuis une sauvegarde externe et mise à jour via {@link #setHighestScore(int)}.
     *
     * @return Le meilleur score entier connu.
     */
    public int getHighestScore() {
        return highestScore;
    }

    /**
     * Met à jour la valeur du meilleur score stockée dans cet objet Game.
     * Cette méthode est typiquement appelée par la classe gérant la persistance
     * (par exemple, MainActivity) après avoir chargé le meilleur score global.
     *
     * @param highScore Le nouveau meilleur score global à stocker dans cette instance.
     */
    public void setHighestScore(int highScore) {
        this.highestScore = highScore;
    }

    /**
     * Vérifie si la condition de victoire a été atteinte (au moins une tuile avec une valeur >= 2048).
     *
     * @return true si le jeu est considéré comme gagné, false sinon.
     */
    public boolean isGameWon() {
        return gameWon;
    }

    /**
     * Vérifie si la partie est terminée.
     * La partie est terminée si aucune case n'est vide ET aucun mouvement ou fusion n'est possible
     * dans aucune des quatre directions.
     *
     * @return true si la partie est terminée (game over), false sinon.
     */
    public boolean isGameOver() {
        return gameOver;
    }

    /**
     * Met à jour l'indicateur interne de victoire du jeu.
     * Utilisé après une fusion ou lors de la restauration d'une partie.
     *
     * @param won true si la condition de victoire est atteinte, false sinon.
     */
    private void setGameWon(boolean won) {
        this.gameWon = won;
    }

    /**
     * Met à jour l'indicateur interne de fin de partie (game over).
     * Utilisé après une tentative de mouvement infructueuse ou lors de la restauration.
     *
     * @param over true si la condition de fin de partie est atteinte, false sinon.
     */
    private void setGameOver(boolean over) {
        this.gameOver = over;
    }

    /**
     * Retourne une copie profonde (deep copy) du plateau de jeu actuel.
     * Ceci est utile pour obtenir l'état du plateau sans risquer de le modifier
     * accidentellement de l'extérieur, ou pour la sérialisation.
     *
     * @return Une nouvelle matrice 2D (`int[BOARD_SIZE][BOARD_SIZE]`) représentant l'état actuel du plateau.
     */
    @NonNull
    public int[][] getBoard() {
        int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
        for (int i = 0; i < BOARD_SIZE; i++) {
            // System.arraycopy est efficace pour copier des tableaux primitifs
            System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
        }
        return copy;
    }

    /**
     * (Ré)Initialise le plateau de jeu pour une nouvelle partie.
     * Crée une nouvelle matrice vide (remplie de 0), réinitialise le score courant
     * et les indicateurs `gameWon`/`gameOver`, puis appelle {@link #addNewTile()}
     * deux fois pour placer les deux premières tuiles aléatoires.
     */
    private void initializeNewBoard() {
        this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice remplie de zéros par défaut
        this.currentScore = 0;
        this.gameWon = false;
        this.gameOver = false;
        // Ajoute les deux premières tuiles requises pour démarrer une partie
        addNewTile();
        addNewTile();
    }

    // --- Logique du Jeu ---

    /**
     * Ajoute une nouvelle tuile (2, 4, 8, etc., selon les probabilités définies
     * dans {@link #generateRandomTileValue()}) sur une case vide choisie aléatoirement.
     * Si le plateau est plein (aucune case vide), cette méthode ne fait rien.
     */
    public void addNewTile() {
        List<int[]> emptyCells = findEmptyCells();
        if (!emptyCells.isEmpty()) {
            // Choisit une cellule vide au hasard parmi celles disponibles
            int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
            int value = generateRandomTileValue();
            // Place la nouvelle tuile sur le plateau logique
            // Utilise setCellValue pour la cohérence, même si l'accès direct serait possible ici
            setCellValue(randomCell[0], randomCell[1], value);
        }
        // Si emptyCells est vide, le plateau est plein, on ne peut rien ajouter.
        // La condition de Game Over sera vérifiée après la tentative de mouvement.
    }

    /**
     * Recherche et retourne les coordonnées de toutes les cellules vides (valeur 0) sur le plateau.
     *
     * @return Une {@link List} de tableaux d'entiers `[row, col]` pour chaque cellule vide trouvée.
     * Retourne une liste vide si le plateau est plein.
     */
    @NonNull
    private List<int[]> findEmptyCells() {
        List<int[]> emptyCells = new ArrayList<>();
        for (int row = 0; row < BOARD_SIZE; row++) {
            for (int col = 0; col < BOARD_SIZE; col++) {
                if (board[row][col] == 0) {
                    emptyCells.add(new int[]{row, col});
                }
            }
        }
        return emptyCells;
    }

    /**
     * Génère la valeur pour une nouvelle tuile (2, 4, 8, ...) en utilisant une distribution
     * de probabilités prédéfinie. Les probabilités sont ajustées pour rendre l'apparition
     * des tuiles de faible valeur plus fréquente.
     *
     * Probabilités actuelles (approximatives) :
     * - 2:   85.40%
     * - 4:   12.00%
     * - 8:    2.00%
     * - 16:   0.50%
     * - 32:   0.05%
     * - 64:   0.03%
     * - 128:  0.01%
     * - 256:  0.01%
     *
     * @return La valeur entière (2, 4, 8, ...) de la tuile générée.
     */
    private int generateRandomTileValue() {
        int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour une granularité fine des pourcentages

        // Les seuils définissent les probabilités cumulées
        if (randomValue < 8540) return 2;   // 0 <= randomValue < 8540 (85.40%)
        if (randomValue < 9740) return 4;   // 8540 <= randomValue < 9740 (12.00%)
        if (randomValue < 9940) return 8;   // 9740 <= randomValue < 9940 (2.00%)
        if (randomValue < 9990) return 16;  // 9940 <= randomValue < 9990 (0.50%)
        if (randomValue < 9995) return 32;  // 9990 <= randomValue < 9995 (0.05%)
        if (randomValue < 9998) return 64;  // 9995 <= randomValue < 9998 (0.03%)
        if (randomValue < 9999) return 128; // 9998 <= randomValue < 9999 (0.01%)
        return 256;                         // 9999 <= randomValue < 10000 (0.01%)
    }

    /**
     * Tente de déplacer et fusionner les tuiles vers le HAUT.
     * Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
     *
     * @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
     */
    public boolean pushUp() {
        return processMove(MoveDirection.UP);
    }

    /**
     * Tente de déplacer et fusionner les tuiles vers le BAS.
     * Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
     *
     * @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
     */
    public boolean pushDown() {
        return processMove(MoveDirection.DOWN);
    }

    /**
     * Tente de déplacer et fusionner les tuiles vers la GAUCHE.
     * Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
     *
     * @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
     */
    public boolean pushLeft() {
        return processMove(MoveDirection.LEFT);
    }

    /**
     * Tente de déplacer et fusionner les tuiles vers la DROITE.
     * Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
     *
     * @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
     */
    public boolean pushRight() {
        return processMove(MoveDirection.RIGHT);
    }

    /** Énumération interne pour clarifier la direction du mouvement dans {@link #processMove}. */
    private enum MoveDirection { UP, DOWN, LEFT, RIGHT }

    /**
     * Méthode générique pour traiter un mouvement (déplacement et fusion) dans une direction donnée.
     * C'est le cœur de la logique de déplacement du jeu. Elle parcourt le plateau, déplace
     * les tuiles jusqu'au bout dans la direction demandée, puis gère les fusions éventuelles.
     *
     * @param direction La direction du mouvement ({@link MoveDirection}).
     * @return true si le plateau a été modifié (au moins un déplacement ou une fusion), false sinon.
     */
    private boolean processMove(MoveDirection direction) {
        boolean boardChanged = false;

        // Itérer sur l'axe perpendiculaire au mouvement (colonnes pour UP/DOWN, lignes pour LEFT/RIGHT)
        for (int i = 0; i < BOARD_SIZE; i++) {
            // Tableau pour suivre si une tuile sur la ligne/colonne cible a déjà résulté d'une fusion
            // pendant CE mouvement, pour éviter les fusions en chaîne (ex: 2-2-4 -> 4-4 -> 8 en un seul swipe)
            boolean[] hasMerged = new boolean[BOARD_SIZE];

            // Itérer sur l'axe du mouvement. Le sens de parcours est crucial :
            // - Pour UP/LEFT: du début vers la fin (index 1 à N-1) car les tuiles fusionnent vers les petits indices.
            // - Pour DOWN/RIGHT: de la fin vers le début (index N-2 à 0) car les tuiles fusionnent vers les grands indices.
            int start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
            int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
            int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;

            for (int j = start; j != end; j += step) {
                int row, col;
                // Déterminer les coordonnées (row, col) de la tuile courante basée sur l'axe i et l'index j
                if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) {
                    row = j; col = i; // Mouvement vertical, i est la colonne, j est la ligne
                } else {
                    row = i; col = j; // Mouvement horizontal, i est la ligne, j est la colonne
                }

                int currentValue = getCellValue(row, col);

                // Si la case n'est pas vide, on essaie de la déplacer/fusionner
                if (currentValue != 0) {
                    int currentRow = row;
                    int currentCol = col;
                    int targetRow = currentRow; // Position cible initiale
                    int targetCol = currentCol;

                    // 1. Déplacement : Trouver la case la plus éloignée atteignable dans la direction du mouvement
                    int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
                    int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);

                    // Tant que la case suivante est valide et vide, on continue de "glisser"
                    while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
                        targetRow = nextRow;
                        targetCol = nextCol;
                        // Calculer la case suivante pour la prochaine itération de la boucle while
                        nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
                        nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
                    }

                    // 2. Fusion : Vérifier la case immédiatement après la position cible (targetRow, targetCol)
                    // La case à vérifier pour fusion est `nextRow`, `nextCol` (calculée juste avant ou après la sortie du while)
                    int mergeTargetRow = nextRow;
                    int mergeTargetCol = nextCol;
                    // Index utilisé pour le tableau `hasMerged` (soit la ligne soit la colonne cible de la fusion)
                    int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;

                    boolean merged = false;
                    // Vérifier si la case de fusion potentielle est valide, a la même valeur, et n'a pas déjà fusionné
                    if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
                            getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
                            !hasMerged[mergeIndex]) // Crucial: empêche double fusion
                    {
                        // Fusion !
                        int newValue = currentValue * 2;
                        setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible de la fusion
                        setCellValue(currentRow, currentCol, 0); // Vide la case d'origine de la tuile qui a bougé ET fusionné
                        currentScore += newValue; // Ajoute la *nouvelle* valeur au score
                        hasMerged[mergeIndex] = true; // Marque la case cible comme ayant fusionné
                        boardChanged = true; // Le plateau a changé
                        merged = true;

                        // Vérifier immédiatement si cette fusion a créé une tuile >= 2048
                        if (newValue >= 2048) {
                            setGameWon(true);
                        }
                    }

                    // 3. Déplacement final (si pas de fusion OU si la tuile a bougé avant de potentiellement fusionner)
                    // Si la tuile n'a pas fusionné (elle reste où elle est ou se déplace seulement)
                    // ET que sa position cible (targetRow, targetCol) est différente de sa position initiale
                    if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
                        setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
                        setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
                        boardChanged = true; // Le plateau a changé
                    }
                } // Fin if (currentValue != 0)
            } // Fin boucle interne (j)
        } // Fin boucle externe (i)

        // Après avoir traité toutes les lignes/colonnes, vérifier l'état global
        // Note: checkWinCondition est déjà appelé lors d'une fusion >= 2048, mais on le refait ici par sécurité.
        checkWinCondition(); // Vérifie si 2048 a été atteint (peut être déjà fait lors d'une fusion)
        checkGameOverCondition(); // Vérifie si plus aucun mouvement n'est possible

        return boardChanged;
    }


    /**
     * Vérifie si les indices de ligne et colonne fournis sont valides pour le plateau de jeu actuel.
     *
     * @param row L'indice de ligne à vérifier.
     * @param col L'indice de colonne à vérifier.
     * @return true si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, false sinon.
     */
    private boolean isIndexValid(int row, int col) {
        return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
    }

    /**
     * Sérialise l'état essentiel du jeu (plateau et score courant) en une chaîne de caractères.
     * Le format actuel est simple : toutes les valeurs du plateau séparées par des virgules,
     * suivies par le score courant, également séparé par une virgule.
     * Exemple pour un plateau 2x2 : "2,0,4,8,12" (où 12 est le score).
     * <p>
     * NOTE : Ce format est simple mais peut être fragile si la structure du jeu
     * ou les données à sauvegarder évoluent. Un format comme JSON pourrait offrir plus de flexibilité.
     * </p>
     *
     * @return Une chaîne non nulle représentant l'état sérialisé du jeu.
     */
    @NonNull
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int row = 0; row < BOARD_SIZE; row++) {
            for (int col = 0; col < BOARD_SIZE; col++) {
                sb.append(board[row][col]).append(",");
            }
        }
        // Ajoute le score à la fin, séparé par une virgule
        sb.append(currentScore);
        return sb.toString();
    }

    /**
     * Crée (désérialise) un objet {@code Game} à partir de sa représentation sous forme de chaîne,
     * telle que générée par {@link #toString()}.
     * Le meilleur score (`highestScore`) n'est pas inclus dans la chaîne et doit être
     * défini séparément sur l'objet retourné via {@link #setHighestScore(int)}.
     * <p>
     * NOTE : Cette méthode dépend du format spécifique généré par {@code toString()}.
     * </p>
     *
     * @param serializedState La chaîne représentant l'état du jeu (plateau + score courant).
     * @return Une nouvelle instance de {@code Game} si la désérialisation réussit,
     * ou {@code null} si la chaîne est invalide, vide, ou a un format incorrect
     * (mauvais nombre d'éléments, valeur non entière).
     */
    @Nullable
    public static Game deserialize(@Nullable String serializedState) {
        if (serializedState == null || serializedState.isEmpty()) {
            return null;
        }
        String[] values = serializedState.split(",");
        // Vérifie si le nombre d'éléments correspond à la taille du plateau + 1 (pour le score)
        if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) {
            return null;
        }

        int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
        int index = 0;
        try {
            // Remplit le plateau à partir des valeurs de la chaîne
            for (int row = 0; row < BOARD_SIZE; row++) {
                for (int col = 0; col < BOARD_SIZE; col++) {
                    newBoard[row][col] = Integer.parseInt(values[index++]);
                }
            }
            // Le dernier élément est le score
            int score = Integer.parseInt(values[index]);

            // Crée une nouvelle instance de Game avec les données désérialisées
            // Le constructeur recalculera gameWon et gameOver.
            return new Game(newBoard, score);

        } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
            // En cas d'erreur de format (valeur non entière, problème d'indice, ou dimension plateau incorrecte dans constructeur)
            // Log l'erreur pourrait être utile pour le débogage
            System.err.println("Erreur lors de la désérialisation de l'état du jeu: " + e.getMessage());
            return null;
        }
    }

    /**
     * Vérifie si la condition de victoire (au moins une tuile avec une valeur >= 2048)
     * est actuellement atteinte sur le plateau. Met à jour l'état interne `gameWon`.
     * Cette vérification s'arrête dès qu'une tuile gagnante est trouvée ou si le jeu
     * est déjà marqué comme gagné.
     */
    private void checkWinCondition() {
        // Si le jeu est déjà marqué comme gagné, pas besoin de revérifier
        if (gameWon) {
            return;
        }
        // Parcours le plateau à la recherche d'une tuile >= 2048
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (getCellValue(r, c) >= 2048) {
                    setGameWon(true); // Met à jour l'état interne
                    return; // Sort dès qu'une condition de victoire est trouvée
                }
            }
        }
        // Si la boucle se termine sans trouver de tuile >= 2048, gameWon reste false (ou son état précédent)
    }

    /**
     * Vérifie si la condition de fin de partie (Game Over) est atteinte.
     * Cela se produit si le plateau est plein (pas de case vide) ET si aucune
     * fusion n'est possible entre des tuiles adjacentes (horizontalement ou verticalement).
     * Met à jour l'état interne `gameOver`.
     */
    private void checkGameOverCondition() {
        // Si le jeu est déjà terminé, pas besoin de revérifier
        if (gameOver) {
            return;
        }
        // Si il y a au moins une case vide, le jeu ne peut pas être terminé
        if (hasEmptyCell()) {
            setGameOver(false);
            return;
        }

        // Le plateau est plein. Vérifier s'il existe au moins une fusion possible.
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                int current = getCellValue(r, c);
                // Vérifie le voisin du HAUT (si existe)
                if (r > 0 && getCellValue(r - 1, c) == current) {
                    setGameOver(false); return; // Fusion possible vers le haut
                }
                // Vérifie le voisin du BAS (si existe)
                if (r < BOARD_SIZE - 1 && getCellValue(r + 1, c) == current) {
                    setGameOver(false); return; // Fusion possible vers le bas
                }
                // Vérifie le voisin de GAUCHE (si existe)
                if (c > 0 && getCellValue(r, c - 1) == current) {
                    setGameOver(false); return; // Fusion possible vers la gauche
                }
                // Vérifie le voisin de DROITE (si existe)
                if (c < BOARD_SIZE - 1 && getCellValue(r, c + 1) == current) {
                    setGameOver(false); return; // Fusion possible vers la droite
                }
            }
        }

        // Si on arrive ici, le plateau est plein ET aucune fusion adjacente n'est possible.
        setGameOver(true);
    }

    /**
     * Vérifie rapidement si le plateau contient au moins une case vide (valeur 0).
     * Utilisé principalement par {@link #checkGameOverCondition()}.
     *
     * @return true s'il existe au moins une case vide, false si le plateau est plein.
     */
    private boolean hasEmptyCell() {
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (getCellValue(r, c) == 0) {
                    return true; // Sort dès qu'une case vide est trouvée
                }
            }
        }
        return false; // Aucune case vide trouvée après avoir parcouru tout le plateau
    }

    /**
     * Retourne la valeur de la plus haute tuile (la plus grande valeur numérique)
     * actuellement présente sur le plateau de jeu.
     *
     * @return La valeur maximale trouvée sur le plateau, ou 0 si le plateau est vide.
     */
    public int getHighestTileValue() {
        int maxTile = 0;
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (board[r][c] > maxTile) {
                    maxTile = board[r][c];
                }
            }
        }
        return maxTile;
    }
}