diff --git a/app/src/main/java/legion/muyue/best2048/Game.java b/app/src/main/java/legion/muyue/best2048/Game.java index fd19f95..818a6ea 100644 --- a/app/src/main/java/legion/muyue/best2048/Game.java +++ b/app/src/main/java/legion/muyue/best2048/Game.java @@ -1,114 +1,121 @@ // Fichier Game.java -// Contient la logique principale du jeu 2048 : gestion du plateau, mouvements, fusions, score. -// Cette classe est maintenant indépendante du contexte Android et de la persistance des données. -/* - Fonctions principales : - - board : Matrice 2D (int[][]) représentant la grille du jeu. - - currentScore : Suivi du score de la partie en cours. - - highestScore : Stocke le meilleur score global (reçu de l'extérieur via setHighestScore). - - addNewTile() : Ajoute une nouvelle tuile aléatoire sur une case vide. - - pushUp(), pushDown(), pushLeft(), pushRight() : Gèrent la logique de déplacement et de fusion, retournent un booléen indiquant si le plateau a changé. - - getHighestTileValue() : Retourne la valeur de la plus haute tuile sur le plateau. - - États gameWon, gameOver : Indiquent si la partie est gagnée ou terminée. - - Méthodes pour vérifier les conditions de victoire et de fin de partie. - - toString(), deserialize() : Sérialisent/désérialisent l'état essentiel du jeu (plateau, score courant) pour la sauvegarde externe. - - Relations : - - MainActivity : Crée une instance de Game, appelle ses méthodes (pushX, addNewTile, getters), lui fournit le meilleur score global via setHighestScore, et utilise son état pour l'affichage et la sauvegarde. -*/ +/** + * Représente la logique métier du jeu 2048. Gère l'état du plateau de jeu, + * les déplacements, les fusions, le score de la partie en cours, et les conditions + * de victoire ou de défaite. Cette classe est conçue pour être indépendante du + * framework Android (pas de dépendance au Contexte ou aux SharedPreferences). + */ package legion.muyue.best2048; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; // Pour les méthodes de test éventuelles + import java.util.ArrayList; import java.util.List; import java.util.Random; public class Game { + /** 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; - private int highestScore = 0; // Stocke le HS fourni par MainActivity + /** Meilleur score global (reçu et stocké, mais non géré logiquement ici). */ + private int highestScore = 0; + /** Taille du plateau de jeu (nombre de lignes/colonnes). */ private static final int BOARD_SIZE = 4; + /** Indicateur si la condition de victoire (>= 2048) a été atteinte. */ private boolean gameWon = false; + /** Indicateur si la partie est terminée (plus de mouvements possibles). */ private boolean gameOver = false; /** - * Constructeur pour une nouvelle partie. - * Initialise un plateau vide, définit le score à 0 et ajoute deux tuiles initiales. + * Constructeur pour démarrer une nouvelle partie. + * Initialise un plateau vide, le score à 0, et ajoute deux tuiles initiales. */ public Game() { this.randomNumberGenerator = new Random(); - // Le highScore sera défini par MainActivity après l'instanciation. initializeNewBoard(); } /** - * Constructeur utilisé lors de la restauration d'une partie sauvegardée. + * 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 fourni. + * * @param board Le plateau de jeu restauré. * @param score Le score courant restauré. */ public Game(int[][] board, int score) { + // Valider les dimensions du plateau fourni ? Pourrait être ajouté. this.board = board; this.currentScore = score; this.randomNumberGenerator = new Random(); - // Le highScore sera défini par MainActivity après l'instanciation. - checkWinCondition(); // Recalcule l'état basé sur le plateau chargé + checkWinCondition(); checkGameOverCondition(); } // --- Getters / Setters --- /** - * Retourne la valeur de la cellule aux coordonnées spécifiées. - * @param row Ligne de la cellule (0-based). - * @param column Colonne de la cellule (0-based). - * @return Valeur de la cellule, ou 0 si indices invalides (sécurité). + * Retourne la valeur de la tuile aux coordonnées spécifiées. + * @param row Ligne (0 à BOARD_SIZE-1). + * @param column Colonne (0 à BOARD_SIZE-1). + * @return Valeur de la tuile, ou 0 si les coordonnées sont invalides. */ public int getCellValue(int row, int column) { - if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { return 0; } - return this.board[row][column]; + if (isIndexValid(row, column)) { + return this.board[row][column]; + } + return 0; // Retourne 0 pour indice invalide } /** - * Définit la valeur de la cellule aux coordonnées spécifiées. - * @param row Ligne de la cellule (0-based). - * @param col Colonne de la cellule (0-based). - * @param value Nouvelle valeur. + * Définit la valeur d'une tuile aux coordonnées spécifiées. + * Ne fait rien si les coordonnées sont invalides. + * @param row Ligne (0 à BOARD_SIZE-1). + * @param col Colonne (0 à BOARD_SIZE-1). + * @param value Nouvelle valeur de la tuile. */ public void setCellValue(int row, int col, int value) { - if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return; } - this.board[row][col] = value; + if (isIndexValid(row, col)) { + this.board[row][col] = value; + } } /** @return Le score actuel de la partie. */ public int getCurrentScore() { return currentScore; } - // public void setCurrentScore(int currentScore) { this.currentScore = currentScore; } // Setter interne si nécessaire - - /** @return Le meilleur score connu par cet objet Game (synchronisé par MainActivity). */ + /** @return Le meilleur score connu par cet objet (défini via setHighestScore). */ public int getHighestScore() { return highestScore; } /** - * Définit le meilleur score global connu. Appelé par MainActivity après chargement - * des préférences ou après une mise à jour du score. - * @param highScore Le meilleur score connu. + * Met à jour la valeur du meilleur score stockée dans cet objet Game. + * Typiquement appelé par la classe gérant la persistance (MainActivity). + * @param highScore Le meilleur score global à stocker. */ public void setHighestScore(int highScore) { this.highestScore = highScore; } - /** @return L'état de victoire de la partie (true si >= 2048 atteint). */ + /** @return true si une tuile 2048 (ou plus) a été atteinte, false sinon. */ public boolean isGameWon() { return gameWon; } - public void setGameWon(boolean gameWon) { this.gameWon = gameWon; } - - /** @return L'état de fin de partie (true si aucun mouvement possible). */ + /** @return true si aucune case n'est vide ET aucun mouvement/fusion n'est possible, false sinon. */ public boolean isGameOver() { return gameOver; } - public void setGameOver(boolean gameOver) { this.gameOver = gameOver; } + /** Met à jour la valeur de gameWon si partie gagné **/ + private void setGameWon(boolean won) {this.gameWon = won;} - /** @return Une copie du plateau de jeu actuel (pour la sauvegarde externe). */ + /** Met à jour la valeur de gameWon si partie gagné **/ + private void setGameOver(boolean over) {this.gameOver = over;} + + /** + * Retourne une copie profonde du plateau de jeu actuel. + * Utile pour la sérialisation ou pour éviter des modifications externes non désirées. + * @return Une nouvelle matrice 2D représentant l'état actuel du plateau. + */ public int[][] getBoard() { - // Retourne une copie pour éviter modifications externes accidentelles int[][] copy = new int[BOARD_SIZE][BOARD_SIZE]; for(int i=0; i 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}); } - } - } + List emptyCells = findEmptyCells(); if (!emptyCells.isEmpty()) { int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size())); int value = generateRandomTileValue(); @@ -151,113 +153,150 @@ public class Game { } /** - * Génère aléatoirement la valeur d'une nouvelle tuile (2, 4, 8, etc.) - * selon des probabilités prédéfinies. - * @return La valeur de la nouvelle tuile. + * Trouve toutes les cellules vides sur le plateau. + * @return Une liste de tableaux d'entiers `[row, col]` pour chaque cellule vide. + */ + private List findEmptyCells() { + List 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 en utilisant des probabilités prédéfinies. + * @return La valeur (2, 4, 8, ...). */ private int generateRandomTileValue() { - int randomValue = randomNumberGenerator.nextInt(10000); - if (randomValue < 8540) return 2; // ~85% - if (randomValue < 9740) return 4; // ~12% - if (randomValue < 9940) return 8; // ~2% - if (randomValue < 9990) return 16; // ~0.5% - // ... (autres probabilités) - if (randomValue < 9995) return 32; - if (randomValue < 9998) return 64; - if (randomValue < 9999) return 128; - return 256; + int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour pourcentages fins + if (randomValue < 8540) return 2; // 85.40% + if (randomValue < 9740) return 4; // 12.00% + if (randomValue < 9940) return 8; // 2.00% + if (randomValue < 9990) return 16; // 0.50% + if (randomValue < 9995) return 32; // 0.05% + if (randomValue < 9998) return 64; // 0.03% + if (randomValue < 9999) return 128;// 0.01% + return 256; // 0.01% } /** * Tente de déplacer et fusionner les tuiles vers le HAUT. - * Met à jour le score interne en cas de fusion. - * Vérifie les conditions de victoire/défaite après le mouvement. - * @return true si au moins une tuile a bougé ou fusionné, false sinon. + * Met à jour le score interne et vérifie les états win/gameOver. + * @return true si le plateau a été modifié, false sinon. */ - public boolean pushUp() { - boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE]; - for (int col = 0; col < BOARD_SIZE; col++) { - hasMerged = new boolean[BOARD_SIZE]; - for (int row = 1; row < BOARD_SIZE; row++) { - if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); int currentRow = row; - while (currentRow > 0 && getCellValue(currentRow - 1, col) == 0) { setCellValue(currentRow - 1, col, currentValue); setCellValue(currentRow, col, 0); currentRow--; boardChanged = true; } - if (currentRow > 0 && getCellValue(currentRow - 1, col) == currentValue && !hasMerged[currentRow - 1]) { - int newValue = getCellValue(currentRow - 1, col) * 2; setCellValue(currentRow - 1, col, newValue); setCellValue(currentRow, col, 0); - currentScore += newValue; hasMerged[currentRow - 1] = true; boardChanged = true; - } - } - } - } checkWinCondition(); checkGameOverCondition(); return boardChanged; - } + public boolean pushUp() { return processMove(MoveDirection.UP); } /** * Tente de déplacer et fusionner les tuiles vers le BAS. - * @return true si changement, false sinon. + * @return true si le plateau a été modifié, false sinon. */ - public boolean pushDown() { - boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE]; - for (int col = 0; col < BOARD_SIZE; col++) { - hasMerged = new boolean[BOARD_SIZE]; - for (int row = BOARD_SIZE - 2; row >= 0; row--) { - if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); int currentRow = row; - while (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == 0) { setCellValue(currentRow + 1, col, currentValue); setCellValue(currentRow, col, 0); currentRow++; boardChanged = true; } - if (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == currentValue && !hasMerged[currentRow + 1]) { - int newValue = getCellValue(currentRow + 1, col) * 2; setCellValue(currentRow + 1, col, newValue); setCellValue(currentRow, col, 0); - currentScore += newValue; hasMerged[currentRow + 1] = true; boardChanged = true; - } - } - } - } checkWinCondition(); checkGameOverCondition(); return boardChanged; - } + public boolean pushDown() { return processMove(MoveDirection.DOWN); } /** * Tente de déplacer et fusionner les tuiles vers la GAUCHE. - * @return true si changement, false sinon. + * @return true si le plateau a été modifié, false sinon. */ - public boolean pushLeft() { - boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE]; - for (int row = 0; row < BOARD_SIZE; row++) { - hasMerged = new boolean[BOARD_SIZE]; - for (int col = 1; col < BOARD_SIZE; col++) { - if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); int currentCol = col; - while (currentCol > 0 && getCellValue(row, currentCol - 1) == 0) { setCellValue(row, currentCol - 1, currentValue); setCellValue(row, currentCol, 0); currentCol--; boardChanged = true; } - if (currentCol > 0 && getCellValue(row, currentCol - 1) == currentValue && !hasMerged[currentCol - 1]) { - int newValue = getCellValue(row, currentCol - 1) * 2; setCellValue(row, currentCol - 1, newValue); setCellValue(row, currentCol, 0); - currentScore += newValue; hasMerged[currentCol - 1] = true; boardChanged = true; - } - } - } - } checkWinCondition(); checkGameOverCondition(); return boardChanged; - } + public boolean pushLeft() { return processMove(MoveDirection.LEFT); } /** * Tente de déplacer et fusionner les tuiles vers la DROITE. - * @return true si changement, false sinon. + * @return true si le plateau a été modifié, false sinon. */ - public boolean pushRight() { - boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE]; - for (int row = 0; row < BOARD_SIZE; row++) { - hasMerged = new boolean[BOARD_SIZE]; - for (int col = BOARD_SIZE - 2; col >= 0; col--) { + public boolean pushRight() { return processMove(MoveDirection.RIGHT); } + + /** Énumération interne pour clarifier le traitement des mouvements. */ + 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. + * Contient la logique de base partagée par les méthodes pushX. + * @param direction La direction du mouvement. + * @return true si le plateau a été modifié, false sinon. + */ + private boolean processMove(MoveDirection direction) { + boolean boardChanged = false; + // Itère sur l'axe perpendiculaire au mouvement + for (int i = 0; i < BOARD_SIZE; i++) { + boolean[] hasMerged = new boolean[BOARD_SIZE]; // Pour éviter double fusion sur l'axe de mouvement + // Itère sur l'axe du mouvement, dans le bon sens + 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 = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? j : i; + int col = (direction == MoveDirection.LEFT || direction == MoveDirection.RIGHT) ? j : i; + if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); int currentCol = col; - while (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == 0) { setCellValue(row, currentCol + 1, currentValue); setCellValue(row, currentCol, 0); currentCol++; boardChanged = true; } - if (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == currentValue && !hasMerged[currentCol + 1]) { - int newValue = getCellValue(row, currentCol + 1) * 2; setCellValue(row, currentCol + 1, newValue); setCellValue(row, currentCol, 0); - currentScore += newValue; hasMerged[currentCol + 1] = true; boardChanged = true; + int currentValue = getCellValue(row, col); + int currentRow = row; + int currentCol = col; + + // Calcule la position cible après déplacement dans les cases vides + int targetRow = currentRow; + int targetCol = currentCol; + int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0); + int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0); + + while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) { + targetRow = nextRow; + targetCol = nextCol; + nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0); + nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0); + } + + // Déplace la tuile si sa position cible est différente + if (targetRow != currentRow || targetCol != currentCol) { + setCellValue(targetRow, targetCol, currentValue); + setCellValue(currentRow, currentCol, 0); + boardChanged = true; + } + + // Vérifie la fusion potentielle avec la case suivante dans la direction du mouvement + int mergeTargetRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0); + int mergeTargetCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0); + int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol; + + if (isIndexValid(mergeTargetRow, mergeTargetCol) && + getCellValue(mergeTargetRow, mergeTargetCol) == currentValue && + !hasMerged[mergeIndex]) + { + int newValue = currentValue * 2; + setCellValue(mergeTargetRow, mergeTargetCol, newValue); + setCellValue(targetRow, targetCol, 0); // La tuile qui fusionne disparaît + currentScore += newValue; + hasMerged[mergeIndex] = true; + boardChanged = true; } } } - } checkWinCondition(); checkGameOverCondition(); return boardChanged; + } + // Vérifie les conditions de fin après chaque type de mouvement complet + checkWinCondition(); + checkGameOverCondition(); + return boardChanged; } /** - * Sérialise l'état actuel du jeu (plateau et score courant) en une chaîne. + * Vérifie si les indices de ligne et colonne sont valides pour le plateau. + * @param row Ligne. + * @param col Colonne. + * @return true si les indices sont dans les limites [0, BOARD_SIZE-1]. + */ + 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) pour sauvegarde. * Format: "val,val,...,val,score" - * @return Chaîne sérialisée. + * @return Chaîne représentant l'état du jeu. */ @NonNull @Override @@ -271,66 +310,70 @@ public class Game { } /** - * Crée un nouvel objet Game à partir d'une chaîne sérialisée (plateau + score). - * @param serializedState Chaîne générée par toString(). - * @return Nouvel objet Game, ou null si la désérialisation échoue. + * Crée un objet Game à partir de sa représentation sérialisée (plateau + score). + * Le meilleur score doit être défini séparément après la création. + * @param serializedState La chaîne issue de {@link #toString()}. + * @return Une nouvelle instance de Game, ou null en cas d'erreur de format. */ public static Game deserialize(String serializedState) { if (serializedState == null || serializedState.isEmpty()) return null; String[] values = serializedState.split(","); - if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; + if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; // +1 pour le score int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0; try { for (int row = 0; row < BOARD_SIZE; row++) { for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); } } - int score = Integer.parseInt(values[index]); + int score = Integer.parseInt(values[index]); // Le dernier élément est le score return new Game(newBoard, score); } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; } } /** - * Vérifie si une tuile >= 2048 existe sur le plateau et met à jour l'état `gameWon`. + * Vérifie si la condition de victoire (une tuile >= 2048) est atteinte. + * Met à jour l'état interne `gameWon`. */ private void checkWinCondition() { - if (!gameWon) { // Optimisation: inutile de revérifier si déjà gagné - for(int r=0; r=2048) { setGameWon(true); return; } + if (!gameWon) { + for (int r=0; r= 2048) { + setGameWon(true); return; + } } } /** - * Vérifie s'il reste des mouvements possibles (case vide ou fusion adjacente). - * Met à jour l'état `gameOver`. + * Vérifie si la condition de fin de partie est atteinte (plateau plein ET aucun mouvement possible). + * Met à jour l'état interne `gameOver`. */ private void checkGameOverCondition() { - if (hasEmptyCell()) { setGameOver(false); return; } // Si case vide, pas game over - // Vérifie fusions adjacentes possibles - for(int r=0; r0 && getCellValue(r-1,c)==current) || (r0 && getCellValue(r,c-1)==current) || (c pas game over } } setGameOver(true); // Aucune case vide et aucune fusion -> game over } /** - * @return true s'il y a au moins une case vide sur le plateau, false sinon. + * Vérifie si le plateau contient au moins une case vide (valeur 0). + * @return true si une case vide existe, false sinon. */ private boolean hasEmptyCell() { - for(int r=0; rmaxTile) maxTile=board[r][c]; + for (int r=0; r maxTile) maxTile = board[r][c]; return maxTile; } } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/GameStats.java b/app/src/main/java/legion/muyue/best2048/GameStats.java index 3b27aa7..5c43b1f 100644 --- a/app/src/main/java/legion/muyue/best2048/GameStats.java +++ b/app/src/main/java/legion/muyue/best2048/GameStats.java @@ -1,30 +1,22 @@ // Fichier GameStats.java -// Gère le stockage, le chargement et la mise à jour des statistiques du jeu 2048. -/* - Fonctions principales : - - Contient tous les champs relatifs aux statistiques (solo et multijoueur). - - loadStats(), saveStats() : Charge et sauvegarde les statistiques via SharedPreferences. - - Méthodes pour mettre à jour les statistiques : startGame(), recordMove(), recordMerge(), recordWin(), recordLoss(), endGame(). - - Getters pour accéder aux valeurs des statistiques. - - formatTime() : Méthode utilitaire pour formater le temps. - - Relations : - - MainActivity : Crée une instance de GameStats, l'utilise pour charger/sauvegarder les stats et met à jour les stats via ses méthodes. Récupère les valeurs via les getters pour l'affichage. - - SharedPreferences : Utilisé pour la persistance des statistiques. -*/ +/** + * Gère la collecte, la persistance (via SharedPreferences) et l'accès + * aux statistiques du jeu 2048, pour les modes solo et multijoueur (si applicable). + */ package legion.muyue.best2048; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import java.util.concurrent.TimeUnit; public class GameStats { - // Clés SharedPreferences (inchangées) + // --- Constantes pour SharedPreferences --- private static final String PREFS_NAME = "Best2048_Prefs"; - private static final String HIGH_SCORE_KEY = "high_score"; + private static final String HIGH_SCORE_KEY = "high_score"; // Clé partagée avec Game/MainActivity + // Clés spécifiques aux statistiques private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed"; - // ... (autres clés inchangées) ... private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted"; private static final String STATS_TOTAL_MOVES = "totalMoves"; private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs"; @@ -34,6 +26,7 @@ public class GameStats { private static final String STATS_PERFECT_GAMES = "perfectGames"; private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs"; private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs"; + // ... (autres clés stats) ... private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon"; private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed"; private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak"; @@ -42,20 +35,27 @@ public class GameStats { private static final String STATS_MP_LOSSES = "totalMultiplayerLosses"; private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore"; - // Champs statistiques (inchangés) + + // --- Champs de Statistiques --- + // Générales & Solo private int totalGamesPlayed; private int totalGamesStarted; private int totalMoves; - private int currentMoves; private long totalPlayTimeMs; - private long currentGameStartTimeMs; - private int mergesThisGame; private int totalMerges; private int highestTile; - private int numberOfTimesObjectiveReached; - private int perfectGames; + private int numberOfTimesObjectiveReached; // Nombre de victoires (>= 2048) + private int perfectGames; // Concept non défini ici private long bestWinningTimeMs; private long worstWinningTimeMs; + private int overallHighScore; // Meilleur score global + + // Partie en cours (non persistées telles quelles) + private int currentMoves; + private long currentGameStartTimeMs; + private int mergesThisGame; + + // Multijoueur private int multiplayerGamesWon; private int multiplayerGamesPlayed; private int multiplayerBestWinningStreak; @@ -63,36 +63,39 @@ public class GameStats { private long multiplayerTotalTimeMs; private int totalMultiplayerLosses; private int multiplayerHighestScore; - private int overallHighScore; + /** Contexte nécessaire pour accéder aux SharedPreferences. */ private final Context context; /** - * Constructeur de GameStats. - * Charge immédiatement les statistiques sauvegardées via SharedPreferences. - * @param context Le contexte de l'application (nécessaire pour SharedPreferences). + * Constructeur. Initialise l'objet et charge immédiatement les statistiques + * depuis les SharedPreferences. + * @param context Contexte de l'application. */ public GameStats(Context context) { - this.context = context; + this.context = context.getApplicationContext(); // Utilise le contexte applicatif loadStats(); } + // --- Persistance (SharedPreferences) --- + /** - * Charge toutes les statistiques (générales et multijoueur) et le high score global - * depuis les SharedPreferences. + * Charge toutes les statistiques persistantes depuis les SharedPreferences. + * Appelé par le constructeur. */ public void loadStats() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0); totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0); totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0); + // ... (chargement de toutes les autres clés persistantes) ... totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0); totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0); totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0); highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0); numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 0); perfectGames = prefs.getInt(STATS_PERFECT_GAMES, 0); - bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); + bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme défaut pour 'best' worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0); multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0); multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0); @@ -104,15 +107,16 @@ public class GameStats { } /** - * Sauvegarde toutes les statistiques (générales et multijoueur) et le high score global - * dans les SharedPreferences. + * Sauvegarde toutes les statistiques persistantes dans les SharedPreferences. + * Appelé typiquement dans `onPause` de l'activité. */ public void saveStats() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(HIGH_SCORE_KEY, overallHighScore); + editor.putInt(HIGH_SCORE_KEY, overallHighScore); // Sauvegarde le HS global editor.putInt(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed); editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted); + // ... (sauvegarde de toutes les autres clés persistantes) ... editor.putInt(STATS_TOTAL_MOVES, totalMoves); editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs); editor.putInt(STATS_TOTAL_MERGES, totalMerges); @@ -128,12 +132,15 @@ public class GameStats { editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs); editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses); editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore); - editor.apply(); + + editor.apply(); // Applique les changements de manière asynchrone } + // --- Méthodes de Mise à Jour des Statistiques --- + /** - * Met à jour les statistiques lors du démarrage d'une nouvelle partie. - * Incrémente le nombre de parties démarrées et réinitialise les compteurs de la partie en cours. + * Doit être appelée au début de chaque nouvelle partie. + * Incrémente le compteur de parties démarrées et réinitialise les stats de la partie en cours. */ public void startGame() { totalGamesStarted++; @@ -143,8 +150,7 @@ public class GameStats { } /** - * Enregistre un mouvement effectué pendant la partie en cours. - * Incrémente le compteur de mouvements de la partie et le compteur total. + * Enregistre un mouvement réussi (qui a modifié le plateau). */ public void recordMove() { currentMoves++; @@ -152,8 +158,8 @@ public class GameStats { } /** - * Enregistre une ou plusieurs fusions survenues pendant la partie en cours. - * @param numberOfMerges Le nombre de fusions à ajouter (idéalement précis). + * Enregistre une ou plusieurs fusions survenues lors d'un mouvement. + * @param numberOfMerges Le nombre de fusions (si connu, sinon 1 par défaut). */ public void recordMerge(int numberOfMerges) { if (numberOfMerges > 0) { @@ -163,8 +169,8 @@ public class GameStats { } /** - * Met à jour la statistique de la plus haute tuile atteinte si la valeur fournie est supérieure. - * @param tileValue La valeur de la tuile candidate. + * Met à jour la statistique de la plus haute tuile atteinte globalement. + * @param tileValue La valeur de la tuile la plus haute de la partie en cours. */ public void updateHighestTile(int tileValue) { if (tileValue > this.highestTile) { @@ -173,27 +179,27 @@ public class GameStats { } /** - * Enregistre une victoire et met à jour les statistiques associées (temps, nombre de victoires). - * @param timeTakenMs Le temps mis pour gagner cette partie en millisecondes. + * Enregistre une victoire et met à jour les temps associés. + * @param timeTakenMs Temps écoulé pour cette partie gagnante. */ public void recordWin(long timeTakenMs) { numberOfTimesObjectiveReached++; - endGame(timeTakenMs); // Finalise les stats de la partie - if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; } if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; } + endGame(timeTakenMs); // Finalise aussi le temps total et parties jouées } /** - * Enregistre une défaite et finalise les statistiques de la partie. + * Enregistre une défaite. */ public void recordLoss() { + // Calcule le temps écoulé avant de finaliser endGame(System.currentTimeMillis() - currentGameStartTimeMs); } /** - * Finalise les statistiques à la fin d'une partie (incrémente parties jouées, ajoute temps de jeu). - * @param timeTakenMs Le temps total de la partie qui vient de se terminer. + * Finalise les statistiques générales à la fin d'une partie (victoire ou défaite). + * @param timeTakenMs Temps total de la partie terminée. */ public void endGame(long timeTakenMs) { totalGamesPlayed++; @@ -201,8 +207,9 @@ public class GameStats { } /** - * Ajoute une durée au temps de jeu total cumulé. - * @param durationMs Durée à ajouter en millisecondes. + * Ajoute une durée (en ms) au temps de jeu total enregistré. + * Typiquement appelé dans `onPause`. + * @param durationMs Durée à ajouter. */ public void addPlayTime(long durationMs) { if (durationMs > 0) { @@ -210,13 +217,13 @@ public class GameStats { } } - // --- Getters --- + // --- Getters pour l'affichage --- public int getTotalGamesPlayed() { return totalGamesPlayed; } public int getTotalGamesStarted() { return totalGamesStarted; } public int getTotalMoves() { return totalMoves; } public int getCurrentMoves() { return currentMoves; } public long getTotalPlayTimeMs() { return totalPlayTimeMs; } - public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } + public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } // Utile pour calcul durée en cours public int getMergesThisGame() { return mergesThisGame; } public int getTotalMerges() { return totalMerges; } public int getHighestTile() { return highestTile; } @@ -224,6 +231,8 @@ public class GameStats { public int getPerfectGames() { return perfectGames; } public long getBestWinningTimeMs() { return bestWinningTimeMs; } public long getWorstWinningTimeMs() { return worstWinningTimeMs; } + public int getOverallHighScore() { return overallHighScore; } // Getter pour HS global + // Getters Multiplayer public int getMultiplayerGamesWon() { return multiplayerGamesWon; } public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; } public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; } @@ -231,32 +240,25 @@ public class GameStats { public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; } public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; } public int getMultiplayerHighestScore() { return multiplayerHighestScore; } - public int getOverallHighScore() { return overallHighScore; } // Getter pour le HS global // --- Setters --- - /** - * Met à jour la valeur interne du meilleur score global. - * Appelé par MainActivity pour synchroniser le high score lu depuis les préférences. - * @param highScore Le meilleur score lu. - */ + /** Met à jour la valeur interne du high score global. */ public void setHighestScore(int highScore) { - // On met à jour seulement si la valeur externe est supérieure, - // car GameStats gère aussi la mise à jour via les scores des parties. - // Ou plus simplement, on fait confiance à la valeur lue par MainActivity. - this.overallHighScore = highScore; + // Met à jour si la nouvelle valeur est meilleure + if (highScore > this.overallHighScore) { + this.overallHighScore = highScore; + // La sauvegarde se fait via saveStats() globalement + } } + /** Définit le timestamp de début de la partie en cours. */ + public void setCurrentGameStartTimeMs(long timeMs) { this.currentGameStartTimeMs = timeMs; } - /** - * Définit le timestamp de démarrage pour la partie en cours. - * @param timeMs Timestamp en millisecondes. - */ - public void setCurrentGameStartTimeMs(long timeMs) { - this.currentGameStartTimeMs = timeMs; - } - - // --- Méthodes calculées --- + // --- Méthodes Calculées --- + /** @return Temps moyen par partie terminée en millisecondes. */ public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; } + /** @return Score moyen par partie multijoueur terminée. */ public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; } + /** @return Temps moyen par partie multijoueur terminée en millisecondes. */ public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; } /** @@ -264,6 +266,7 @@ public class GameStats { * @param milliseconds Durée en millisecondes. * @return Chaîne de temps formatée. */ + @SuppressLint("DefaultLocale") public static String formatTime(long milliseconds) { long hours = TimeUnit.MILLISECONDS.toHours(milliseconds); long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index 363041f..fbba04b 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -1,32 +1,14 @@ // Fichier MainActivity.java -// Activité principale de l'application 2048, gère l'interface utilisateur et la coordination du jeu. -/* - Fonctions principales : - - findViews() : Récupère les références des éléments UI du layout XML. - - initializeGameAndStats() : Crée les instances de Game et GameStats, charge les données sauvegardées. - - setupListeners() : Attache les listeners aux boutons et au plateau de jeu (pour les swipes). - - updateUI(), updateBoard(), updateScores() : Met à jour l'affichage du jeu (plateau, scores). - - setTileStyle() : Applique le style visuel à une tuile individuelle. - - handleSwipe() : Traite un geste de swipe, appelle la logique de Game, met à jour les stats et l'UI. - - startNewGame() : Initialise une nouvelle partie. - - showRestartConfirmationDialog(), showGameWonDialog(), showGameOverDialog(), showMenu(), showMultiplayerScreen() : Affiche diverses boîtes de dialogue. - - toggleStatistics(), updateStatisticsTextViews() : Gère l'affichage et la mise à jour du panneau de statistiques (via ViewStub). - - Cycle de vie : onCreate(), onResume(), onPause() pour initialiser, reprendre le timer, et sauvegarder l'état/stats. - - Persistance : Utilise SharedPreferences pour sauvegarder/charger l'état du jeu (via Game.toString/deserialize) et le meilleur score. Les stats détaillées sont gérées par GameStats. - - Relations : - - Game : Instance contenant la logique pure du jeu (plateau, score, mouvements). - - GameStats : Instance contenant la logique et les données des statistiques. - - OnSwipeTouchListener : Détecte les swipes sur le plateau. - - Layout XML (activity_main.xml, stats_layout.xml, etc.) : Définit la structure de l'UI. - - Ressources (strings, colors, dimens, styles, drawables) : Utilisées pour l'apparence de l'UI. - - SharedPreferences : Pour la sauvegarde des données persistantes. -*/ +/** + * Activité principale de l'application 2048. + * Gère l'interface utilisateur (plateau, scores, boutons), coordonne les interactions + * avec la logique du jeu (classe Game) et la gestion des statistiques (classe GameStats). + * Gère également le cycle de vie de l'application et la persistance de l'état du jeu. + */ package legion.muyue.best2048; import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.util.TypedValue; @@ -46,7 +28,7 @@ import android.widget.Button; public class MainActivity extends AppCompatActivity { - // --- UI Elements --- + private GridLayout boardGridLayout; private TextView currentScoreTextView; private TextView highestScoreTextView; @@ -55,25 +37,25 @@ public class MainActivity extends AppCompatActivity { private Button statisticsButton; private Button menuButton; private ViewStub statisticsViewStub; - private View inflatedStatsView; // Référence à la vue des stats une fois gonflée + private View inflatedStatsView; + - // --- Game Logic & Stats --- private Game game; - private GameStats gameStats; // Instance pour gérer les stats + private GameStats gameStats; private static final int BOARD_SIZE = 4; - // --- State Management --- - private boolean statisticsVisible = false; - private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } // Nouvel état de jeu - private GameFlowState currentGameState = GameFlowState.PLAYING; // Initialisation - // --- Preferences --- + private boolean statisticsVisible = false; + private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } + private GameFlowState currentGameState = GameFlowState.PLAYING; + + private SharedPreferences preferences; private static final String PREFS_NAME = "Best2048_Prefs"; private static final String HIGH_SCORE_KEY = "high_score"; private static final String GAME_STATE_KEY = "game_state"; - // --- Activity Lifecycle --- + @Override protected void onCreate(Bundle savedInstanceState) { @@ -81,26 +63,25 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - findViews(); // Récupère les vues - initializeGameAndStats(); // Initialise Game, GameStats et charge les données - setupListeners(); // Attache les listeners + findViews(); + initializeGameAndStats(); + setupListeners(); } @Override protected void onResume() { super.onResume(); - // Redémarre le chrono pour la partie en cours SI elle n'est pas finie if (game != null && gameStats != null && !game.isGameOver() && !game.isGameWon()) { - gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis()); // Utilise setter de GameStats + gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis()); } - // Gère le réaffichage potentiel des stats si l'activité reprend + if (statisticsVisible) { - if (inflatedStatsView != null) { // Si déjà gonflé - updateStatisticsTextViews(); // Met à jour les données affichées + if (inflatedStatsView != null) { + updateStatisticsTextViews(); inflatedStatsView.setVisibility(View.VISIBLE); multiplayerButton.setVisibility(View.GONE); } else { - // Si pas encore gonflé (cas rare mais possible), on le fait afficher + toggleStatistics(); } } @@ -109,18 +90,18 @@ public class MainActivity extends AppCompatActivity { @Override protected void onPause() { super.onPause(); - // Sauvegarde l'état et les stats si le jeu existe + if (game != null && gameStats != null) { - // Met à jour le temps total SI la partie n'est pas terminée + if (!game.isGameOver() && !game.isGameWon()) { - gameStats.addPlayTime(System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs()); // Utilise méthode GameStats + gameStats.addPlayTime(System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs()); } - saveGame(); // Sauvegarde l'état du jeu (plateau + score courant) et le HS - gameStats.saveStats(); // Sauvegarde toutes les stats via GameStats + saveGame(); + gameStats.saveStats(); } } - // --- Initialisation --- + /** * Récupère les références des vues du layout principal via leur ID. @@ -144,20 +125,20 @@ public class MainActivity extends AppCompatActivity { private void initializeGameAndStats() { preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); gameStats = new GameStats(this); - loadGame(); // Charge jeu et met à jour high score + loadGame(); updateUI(); if (game == null) { - startNewGame(); // Assure une partie valide si chargement échoue + startNewGame(); } else { - // Détermine l'état initial basé sur le jeu chargé + if (game.isGameOver()) { currentGameState = GameFlowState.GAME_OVER; } else if (game.isGameWon()) { - // Si on charge une partie déjà gagnée, on considère qu'on a déjà vu la dialog + currentGameState = GameFlowState.WON_DIALOG_SHOWN; } else { currentGameState = GameFlowState.PLAYING; - // Redémarre le timer dans onResume + } } } @@ -172,20 +153,20 @@ public class MainActivity extends AppCompatActivity { }); statisticsButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - toggleStatistics(); // Affiche/masque les stats + toggleStatistics(); }); menuButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showMenu(); // Affiche dialogue placeholder + showMenu(); }); multiplayerButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showMultiplayerScreen(); // Affiche dialogue placeholder + showMultiplayerScreen(); }); - setupSwipeListener(); // Attache le listener de swipe + setupSwipeListener(); } - // --- Mise à jour UI --- + /** * Met à jour complètement l'interface utilisateur (plateau et scores). @@ -205,16 +186,16 @@ public class MainActivity extends AppCompatActivity { for (int col = 0; col < BOARD_SIZE; col++) { TextView tileTextView = new TextView(this); int value = game.getCellValue(row, col); - setTileStyle(tileTextView, value); // Applique le style - // Définit les LayoutParams pour que la tuile remplisse la cellule du GridLayout + setTileStyle(tileTextView, value); + GridLayout.LayoutParams params = new GridLayout.LayoutParams(); - params.width = 0; params.height = 0; // Poids gère la taille - params.rowSpec = GridLayout.spec(row, 1f); // Prend 1 fraction de l'espace en hauteur - params.columnSpec = GridLayout.spec(col, 1f); // Prend 1 fraction de l'espace en largeur + params.width = 0; params.height = 0; + params.rowSpec = GridLayout.spec(row, 1f); + params.columnSpec = GridLayout.spec(col, 1f); int margin = (int) getResources().getDimension(R.dimen.tile_margin); - params.setMargins(margin, margin, margin, margin); // Applique les marges + params.setMargins(margin, margin, margin, margin); tileTextView.setLayoutParams(params); - boardGridLayout.addView(tileTextView); // Ajoute la tuile au GridLayout + boardGridLayout.addView(tileTextView); } } } @@ -233,7 +214,7 @@ public class MainActivity extends AppCompatActivity { * @param value La valeur numérique de la tuile (0 pour vide). */ private void setTileStyle(TextView tileTextView, int value) { - // Code de styling (inchangé par rapport à la correction précédente) + tileTextView.setText(value > 0 ? String.valueOf(value) : ""); tileTextView.setGravity(Gravity.CENTER); tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); @@ -260,7 +241,7 @@ public class MainActivity extends AppCompatActivity { } - // --- Gestion des Actions Utilisateur --- + /** * Configure le listener pour détecter les swipes sur le plateau de jeu. @@ -289,20 +270,19 @@ public class MainActivity extends AppCompatActivity { * @param direction La direction du swipe détecté (UP, DOWN, LEFT, RIGHT). */ private void handleSwipe(Direction direction) { - // Si le jeu n'est pas initialisé ou s'il est DÉJÀ terminé, ignorer le swipe. + if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) { return; } - // Stocker le score avant le mouvement pour calculer le delta + int scoreBefore = game.getCurrentScore(); - // Indique si le mouvement a effectivement changé l'état du plateau + boolean boardChanged = false; - // --- 1. Tenter d'effectuer le mouvement --- - // Les méthodes pushX() de l'objet Game contiennent la logique de déplacement/fusion - // et appellent en interne checkWinCondition() et checkGameOverCondition() - // pour mettre à jour les états isGameWon() et isGameOver(). + + + switch (direction) { case UP: boardChanged = game.pushUp(); @@ -318,63 +298,63 @@ public class MainActivity extends AppCompatActivity { break; } - // --- 2. Traiter les conséquences SI le plateau a changé --- + if (boardChanged) { - // Mettre à jour les statistiques liées au mouvement réussi + gameStats.recordMove(); int scoreAfter = game.getCurrentScore(); int scoreDelta = scoreAfter - scoreBefore; if (scoreDelta > 0) { - // Supposition simpliste : une augmentation de score implique au moins une fusion + gameStats.recordMerge(1); - // Vérifier et mettre à jour le meilleur score si nécessaire + if (scoreAfter > game.getHighestScore()) { - game.setHighestScore(scoreAfter); // Met à jour dans l'objet Game - gameStats.setHighestScore(scoreAfter); // Met à jour et sauvegarde dans GameStats + game.setHighestScore(scoreAfter); + gameStats.setHighestScore(scoreAfter); } } - // Mettre à jour la tuile la plus haute atteinte + gameStats.updateHighestTile(game.getHighestTileValue()); - // Ajouter une nouvelle tuile aléatoire sur le plateau + game.addNewTile(); - // Mettre à jour l'affichage complet du plateau et des scores + updateUI(); } - // --- 3. Vérifier l'état final du jeu (Gagné / Perdu) --- - // Cette vérification est faite APRÈS la tentative de mouvement, - // On vérifie aussi qu'on n'a pas DÉJÀ traité la fin de partie dans ce même appel. + + + if (currentGameState != GameFlowState.GAME_OVER) { - // a) Condition de Victoire (atteindre 2048 ou plus) - // On vérifie aussi qu'on était en train de jouer normalement (pas déjà gagné et décidé de continuer) + + if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) { - currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Mettre à jour l'état de flux - // Enregistrer les statistiques de victoire + currentGameState = GameFlowState.WON_DIALOG_SHOWN; + long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); gameStats.recordWin(timeTaken); - // Afficher la boîte de dialogue de victoire + showGameWonKeepPlayingDialog(); - // b) Condition de Défaite (Game Over - pas de case vide ET pas de fusion possible) - // Cette condition est vérifiée seulement si on n'a pas déjà gagné. + + } else if (game.isGameOver()) { - currentGameState = GameFlowState.GAME_OVER; // Mettre à jour l'état de flux - // Enregistrer les statistiques de défaite et finaliser la partie + currentGameState = GameFlowState.GAME_OVER; + long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); gameStats.recordLoss(); - gameStats.endGame(timeTaken); // Finalise le temps, etc. - // Afficher la boîte de dialogue de Game Over + gameStats.endGame(timeTaken); + showGameOverDialog(); if (!boardChanged) { - updateUI(); // Assure que le score final affiché est correct. + updateUI(); } } - // c) Ni gagné, ni perdu : Le jeu continue. L'état reste PLAYING ou WON_DIALOG_SHOWN. + } } @@ -396,11 +376,11 @@ public class MainActivity extends AppCompatActivity { * crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI. */ private void startNewGame() { - gameStats.startGame(); // Réinitialise stats de partie - game = new Game(); // Crée un nouveau jeu - game.setHighestScore(gameStats.getOverallHighScore()); // Applique HS global - currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER - updateUI(); // Met à jour affichage + gameStats.startGame(); + game = new Game(); + game.setHighestScore(gameStats.getOverallHighScore()); + currentGameState = GameFlowState.PLAYING; + updateUI(); } /** @@ -410,24 +390,24 @@ public class MainActivity extends AppCompatActivity { private void showGameWonKeepPlayingDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); - View dialogView = inflater.inflate(R.layout.dialog_game_won, null); // Gonfle le layout personnalisé + View dialogView = inflater.inflate(R.layout.dialog_game_won, null); builder.setView(dialogView); - builder.setCancelable(false); // Empêche de fermer sans choisir + builder.setCancelable(false); + - // Récupère les boutons DANS la vue gonflée Button keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton); Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon); final AlertDialog dialog = builder.create(); keepPlayingButton.setOnClickListener(v -> { - // L'état est déjà WON_DIALOG_SHOWN, on ne fait rien de spécial, le jeu continue. + dialog.dismiss(); }); newGameButton.setOnClickListener(v -> { dialog.dismiss(); - startNewGame(); // Démarre une nouvelle partie + startNewGame(); }); dialog.show(); @@ -440,35 +420,35 @@ public class MainActivity extends AppCompatActivity { private void showGameOverDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); - View dialogView = inflater.inflate(R.layout.dialog_game_over, null); // Gonfle le layout personnalisé + View dialogView = inflater.inflate(R.layout.dialog_game_over, null); builder.setView(dialogView); - builder.setCancelable(false); // Empêche de fermer sans choisir + builder.setCancelable(false); + - // Récupère les vues DANS la vue gonflée TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver); Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver); Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver); - // Met à jour le message avec le score final + messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore())); final AlertDialog dialog = builder.create(); newGameButton.setOnClickListener(v -> { dialog.dismiss(); - startNewGame(); // Démarre une nouvelle partie + startNewGame(); }); quitButton.setOnClickListener(v -> { dialog.dismiss(); - finish(); // Ferme l'application + finish(); }); dialog.show(); } - // --- Gestion des Statistiques (UI) --- + /** * Affiche ou masque le panneau de statistiques. @@ -477,20 +457,20 @@ public class MainActivity extends AppCompatActivity { private void toggleStatistics() { statisticsVisible = !statisticsVisible; if (statisticsVisible) { - if (inflatedStatsView == null) { // Gonfle si pas encore fait + if (inflatedStatsView == null) { inflatedStatsView = statisticsViewStub.inflate(); - // Attache listener au bouton Back une fois la vue gonflée + Button backButton = inflatedStatsView.findViewById(R.id.backButton); - backButton.setOnClickListener(v -> toggleStatistics()); // Recliquer sur Back re-appelle toggle + backButton.setOnClickListener(v -> toggleStatistics()); } - updateStatisticsTextViews(); // Remplit les champs avec les données actuelles - inflatedStatsView.setVisibility(View.VISIBLE); // Affiche - multiplayerButton.setVisibility(View.GONE); // Masque bouton multi + updateStatisticsTextViews(); + inflatedStatsView.setVisibility(View.VISIBLE); + multiplayerButton.setVisibility(View.GONE); } else { - if (inflatedStatsView != null) { // Masque si la vue existe + if (inflatedStatsView != null) { inflatedStatsView.setVisibility(View.GONE); } - multiplayerButton.setVisibility(View.VISIBLE); // Réaffiche bouton multi + multiplayerButton.setVisibility(View.VISIBLE); } } @@ -501,7 +481,7 @@ public class MainActivity extends AppCompatActivity { private void updateStatisticsTextViews() { if (inflatedStatsView == null || gameStats == null) return; - // Récupération des TextViews dans la vue gonflée (idem) + TextView highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label); TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label); TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_label); @@ -522,12 +502,12 @@ public class MainActivity extends AppCompatActivity { TextView multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label); TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label); TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_label); - TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label); // ID toujours potentiellement dupliqué + TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label); TextView totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label); TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label); TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game); - // MAJ textes avec getters de gameStats + highScoreStatsLabel.setText(getString(R.string.high_score_stats, gameStats.getOverallHighScore())); totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed())); totalGamesStartedLabel.setText(getString(R.string.total_games_started, gameStats.getTotalGamesStarted())); @@ -545,13 +525,13 @@ public class MainActivity extends AppCompatActivity { totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, gameStats.getTotalMultiplayerLosses())); multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, gameStats.getMultiplayerHighestScore())); - // Calculs Pourcentages + String winPercentage = (gameStats.getTotalGamesStarted() > 0) ? String.format("%.2f%%", ((double) gameStats.getNumberOfTimesObjectiveReached() / gameStats.getTotalGamesStarted()) * 100) : "N/A"; winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage)); String multiplayerWinRate = (gameStats.getMultiplayerGamesPlayed() > 0) ? String.format("%.2f%%", ((double) gameStats.getMultiplayerGamesWon() / gameStats.getMultiplayerGamesPlayed()) * 100) : "N/A"; multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate)); - // Calculs Temps + totalPlayTimeLabel.setText(getString(R.string.total_play_time, GameStats.formatTime(gameStats.getTotalPlayTimeMs()))); long currentDuration = (game != null && !game.isGameOver() && !game.isGameWon() && gameStats != null) ? System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs() : 0; currentGameTimeLabel.setText(getString(R.string.current_game_time, GameStats.formatTime(currentDuration))); @@ -561,32 +541,32 @@ public class MainActivity extends AppCompatActivity { worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (gameStats.getWorstWinningTimeMs() != 0) ? GameStats.formatTime(gameStats.getWorstWinningTimeMs()) : "N/A")); } - // --- Dialogues / Placeholders --- + /** Affiche un dialogue placeholder pour le menu. */ - private void showMenu() { /* ... (inchangé) ... */ + private void showMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Menu").setMessage("Fonctionnalité de menu à venir !").setPositiveButton("OK", null); builder.create().show(); } /** Affiche un dialogue placeholder pour le multijoueur. */ - private void showMultiplayerScreen() { /* ... (inchangé) ... */ + private void showMultiplayerScreen() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null); builder.create().show(); } - // --- Sauvegarde / Chargement --- + /** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */ private void saveGame() { SharedPreferences.Editor editor = preferences.edit(); if (game != null) { - editor.putString(GAME_STATE_KEY, game.toString()); // Sérialise Game (plateau + score courant) - editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); // Sauvegarde le HS contenu dans Game + editor.putString(GAME_STATE_KEY, game.toString()); + editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); } else { - editor.remove(GAME_STATE_KEY); // Optionnel: nettoyer si pas de jeu + editor.remove(GAME_STATE_KEY); } editor.apply(); } @@ -594,28 +574,27 @@ public class MainActivity extends AppCompatActivity { /** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */ private void loadGame() { String gameStateString = preferences.getString(GAME_STATE_KEY, null); - int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0); // HS lu depuis prefs + int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0); - if (gameStats != null) { // S'assure que GameStats a le HS correct + if (gameStats != null) { gameStats.setHighestScore(savedHighScore); } if (gameStateString != null) { game = Game.deserialize(gameStateString); if (game != null) { - game.setHighestScore(savedHighScore); // Applique HS à Game - // Détermine l'état basé sur le jeu chargé - if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER; - else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Si gagné avant, on continue - else currentGameState = GameFlowState.PLAYING; - } else { game = null; } // Échec désérialisation - } else { game = null; } // Pas de sauvegarde + game.setHighestScore(savedHighScore); - if (game == null) { // Si pas de jeu chargé ou erreur + if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER; + else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN; + else currentGameState = GameFlowState.PLAYING; + } else { game = null; } + } else { game = null; } + + if (game == null) { game = new Game(); game.setHighestScore(savedHighScore); currentGameState = GameFlowState.PLAYING; - // Pas besoin d'appeler gameStats.startGame() ici, sera fait dans initializeGame OU startNewGame si nécessaire } } diff --git a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java index 082408f..a530780 100644 --- a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java +++ b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java @@ -1,151 +1,125 @@ // Fichier OnSwipeTouchListener.java -// Classe utilitaire pour détecter les gestes de balayage (swipe). -/* - Fonctions principales : - - Utilise GestureDetector pour détecter les swipes. - - Définit une interface SwipeListener pour notifier la classe appelante (MainActivity). - - onFling() : Détecte la direction du swipe (haut, bas, gauche, droite). - - SWIPE_THRESHOLD, SWIPE_VELOCITY_THRESHOLD : Constantes pour la sensibilité du swipe. - - Relations : - - MainActivity : MainActivity crée une instance et est notifiée des swipes via l'interface SwipeListener. -*/ - +/** + * Listener de vue personnalisé qui détecte les gestes de balayage (swipe) + * dans les quatre directions cardinales et notifie un listener externe. + * Utilise {@link GestureDetector} pour l'analyse des gestes. + */ package legion.muyue.best2048; -import android.annotation.SuppressLint; // Ajout +import android.annotation.SuppressLint; import android.content.Context; -// import android.location.GnssAntennaInfo; // Import inutile trouvé dans le dump import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; - -import androidx.annotation.NonNull; // Ajout +import androidx.annotation.NonNull; public class OnSwipeTouchListener implements View.OnTouchListener { + /** Détecteur de gestes standard d'Android. */ private final GestureDetector gestureDetector; + /** Listener externe à notifier lors de la détection d'un swipe. */ private final SwipeListener listener; /** - * Interface pour les événements de swipe. Toute classe qui veut réagir aux swipes - * doit implémenter cette interface et passer une instance à OnSwipeTouchListener. + * Interface à implémenter par les classes souhaitant réagir aux événements de swipe. */ public interface SwipeListener { - /** - * Appelée lorsqu'un swipe vers le haut est détecté. - */ + /** Appelée lorsqu'un swipe vers le haut est détecté. */ void onSwipeTop(); - - /** - * Appelée lorsqu'un swipe vers le bas est détecté. - */ + /** Appelée lorsqu'un swipe vers le bas est détecté. */ void onSwipeBottom(); - - /** - * Appelée lorsqu'un swipe vers la gauche est détecté. - */ + /** Appelée lorsqu'un swipe vers la gauche est détecté. */ void onSwipeLeft(); - - /** - * Appelée lorsqu'un swipe vers la droite est détecté. - */ + /** Appelée lorsqu'un swipe vers la droite est détecté. */ void onSwipeRight(); } /** - * Constructeur de la classe OnSwipeTouchListener. - * - * @param ctx Le contexte de l'application. Nécessaire pour GestureDetector. - * @param listener L'instance qui écoutera les événements de swipe. + * Constructeur. + * @param context Contexte applicatif, nécessaire pour `GestureDetector`. + * @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null. */ - public OnSwipeTouchListener(Context ctx, SwipeListener listener) { - gestureDetector = new GestureDetector(ctx, new GestureListener()); + public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) { + this.gestureDetector = new GestureDetector(context, new GestureListener()); this.listener = listener; } /** - * Méthode appelée lorsqu'un événement tactile se produit sur la vue attachée. - * Elle transmet l'événement au GestureDetector pour analyse. - * - * @param v La vue sur laquelle l'événement tactile s'est produit. - * @param event L'objet MotionEvent décrivant l'événement tactile. - * @return true si l'événement a été géré, false sinon. + * Intercepte les événements tactiles sur la vue associée et les délègue + * au {@link GestureDetector} pour analyse. + * @param v La vue touchée. + * @param event L'événement tactile. + * @return true si le geste a été consommé par le détecteur, false sinon. */ - @SuppressLint("ClickableViewAccessibility") // Ajout + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { + // Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling), + // il retournera true, et l'événement ne sera pas propagé davantage. return gestureDetector.onTouchEvent(event); } /** - * Classe interne qui étend GestureDetector.SimpleOnGestureListener pour gérer - * spécifiquement les gestes de balayage (fling). + * Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide). */ private final class GestureListener extends GestureDetector.SimpleOnGestureListener { - private static final int SWIPE_THRESHOLD = 100; // Distance minimale du swipe (en pixels). - private static final int SWIPE_VELOCITY_THRESHOLD = 100; // Vitesse minimale du swipe (en pixels par seconde). + /** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */ + private static final int SWIPE_THRESHOLD = 100; + /** Vitesse minimale (en pixels/sec) pour qu'un mouvement soit considéré comme un swipe. */ + private static final int SWIPE_VELOCITY_THRESHOLD = 100; /** - * Méthode appelée lorsqu'un appui initial sur l'écran est détecté. - * On retourne toujours 'true' pour indiquer qu'on gère cet événement. - * - * @param e L'objet MotionEvent décrivant l'appui initial. - * @return true, car on gère toujours l'événement 'onDown'. + * Toujours retourner true pour onDown garantit que les événements suivants + * (comme onFling) seront bien reçus par ce listener. */ @Override - public boolean onDown(@NonNull MotionEvent e) { // Ajout @NonNull + public boolean onDown(@NonNull MotionEvent e) { return true; } /** - * Méthode appelée lorsqu'un geste de balayage (fling) est détecté. - * - * @param e1 L'objet MotionEvent du premier appui (début du swipe). Peut être null. - * @param e2 L'objet MotionEvent de la fin du swipe. - * @param velocityX La vitesse du swipe en pixels par seconde sur l'axe X. - * @param velocityY La vitesse du swipe en pixels par seconde sur l'axe Y. - * @return true si le geste de balayage a été géré, false sinon. + * Appelée quand un geste de 'fling' (balayage rapide) est détecté. + * Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide + * et notifie le {@link SwipeListener} externe. */ @Override - public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { // Ajout @NonNull - // Vérification de nullité pour e1, nécessaire car onFling peut être appelé même si onDown retourne false ou si le geste est complexe. - if (e1 == null) { - return false; - } + public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { + if (e1 == null) return false; // Point de départ est nécessaire boolean result = false; try { - float diffY = e2.getY() - e1.getY(); // Différence de position sur l'axe Y. - float diffX = e2.getX() - e1.getX(); // Différence de position sur l'axe X. + float diffY = e2.getY() - e1.getY(); + float diffX = e2.getX() - e1.getX(); - // Détermine si le swipe est plutôt horizontal ou vertical. + // Priorité au mouvement le plus ample (horizontal ou vertical) if (Math.abs(diffX) > Math.abs(diffY)) { - // Swipe horizontal. + // Mouvement principalement horizontal if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { if (diffX > 0) { - listener.onSwipeRight(); // Swipe vers la droite. + listener.onSwipeRight(); } else { - listener.onSwipeLeft(); // Swipe vers la gauche. + listener.onSwipeLeft(); } - result = true; + result = true; // Geste horizontal traité } } else { - // Swipe vertical. + // Mouvement principalement vertical if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { if (diffY > 0) { - listener.onSwipeBottom(); // Swipe vers le bas. + listener.onSwipeBottom(); } else { - listener.onSwipeTop(); // Swipe vers le haut. + listener.onSwipeTop(); } - result = true; + result = true; // Geste vertical traité } } } catch (Exception exception) { - exception.fillInStackTrace(); // Gestion des erreurs (journalisation). + // En cas d'erreur inattendue, on logue discrètement. + System.err.println("Erreur dans OnSwipeTouchListener.onFling: " + exception.getMessage()); + // Ne pas crasher l'application pour une erreur de détection de geste. } return result; } } -} \ No newline at end of file +} // Fin OnSwipeTouchListener \ No newline at end of file