From 9d8d2c5c629dbb3112e4fe6f50715c09c4fb2461 Mon Sep 17 00:00:00 2001 From: Muyue Date: Fri, 4 Apr 2025 13:12:59 +0200 Subject: [PATCH] Refactor: Finalisation et externalisation des statistiques MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création de la classe GameStats pour encapsuler les données et la logique des statistiques. - Implémentation de loadStats() et saveStats() dans GameStats utilisant SharedPreferences. - Ajout de méthodes dans GameStats pour enregistrer les événements du jeu (startGame, recordMove, recordMerge, recordWin, recordLoss, endGame, updateHighestTile, addPlayTime, setCurrentGameStartTimeMs, setHighestScore). - Ajout de getters dans GameStats pour l'affichage et de méthodes pour les valeurs calculées (moyennes, pourcentages). - Déplacement de formatTime() dans GameStats. - Refactorisation de MainActivity: - Suppression des champs et méthodes de statistiques individuelles. - Utilisation d'une instance de GameStats pour gérer les statistiques. - Mise à jour de handleSwipe, startNewGame, onPause, onResume pour appeler GameStats. - Mise à jour de updateStatisticsTextViews pour utiliser les getters de GameStats. - MainActivity gère maintenant le chargement/sauvegarde de l'état sérialisé du jeu et du high score via SharedPreferences, en passant le high score à Game via setHighestScore. - Refactorisation de Game: - Suppression de la dépendance au Contexte et SharedPreferences. - Suppression de la gestion interne du high score (reçoit via setHighestScore). - Ajout de getBoard() et getHighestTileValue(). - Modification du constructeur et de deserialize pour être indépendants du contexte et du high score. - Ajout/Mise à jour des commentaires JavaDoc dans les classes modifiées. --- .../main/java/legion/muyue/best2048/Game.java | 682 ++++--------- .../java/legion/muyue/best2048/GameStats.java | 274 ++++++ .../legion/muyue/best2048/MainActivity.java | 899 +++++++----------- .../muyue/best2048/OnSwipeTouchListener.java | 1 - 4 files changed, 803 insertions(+), 1053 deletions(-) create mode 100644 app/src/main/java/legion/muyue/best2048/GameStats.java diff --git a/app/src/main/java/legion/muyue/best2048/Game.java b/app/src/main/java/legion/muyue/best2048/Game.java index eb9df2c..fd19f95 100644 --- a/app/src/main/java/legion/muyue/best2048/Game.java +++ b/app/src/main/java/legion/muyue/best2048/Game.java @@ -1,640 +1,336 @@ // Fichier Game.java -// Ce fichier contient la logique principale du jeu 2048. Il est indépendant de l'interface utilisateur. +// 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 : - - gameBoard : Matrice 2D (int[][]) représentant la grille du jeu. - - score, highScore : Suivi du score et du meilleur score. - - addNewNumbers() : Ajoute une nouvelle tuile (2, 4 ou 8) aléatoirement sur la grille. - - pushUp(), pushDown(), pushLeft(), pushRight() : Logique de déplacement et de fusion des tuiles. - - serialize(), deserialize() : Sauvegarde et restauration de l'état du jeu (grille, score, meilleur score) en chaînes de caractères. - - loadHighScore(), saveHighScore(), loadGameState(), saveGameState() : Utilisation de SharedPreferences pour persister les données. - - Constructeurs : Initialisation de la grille et chargement/restauration des données. + - 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 et appelle ses méthodes pour la logique du jeu. MainActivity affiche l'état du jeu. - - SharedPreferences : Game utilise SharedPreferences pour sauvegarder et charger le meilleur score et l'état du jeu. + - 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. */ - package legion.muyue.best2048; -import android.content.Context; -import android.content.SharedPreferences; - import androidx.annotation.NonNull; - import java.util.ArrayList; import java.util.List; import java.util.Random; public class Game { - private int[][] board; // Renommé - private final Random randomNumberGenerator; // Renommé - private int currentScore = 0; // Renommé - private int highestScore = 0; // Renommé - private static final int BOARD_SIZE = 4; // Ajout constante - private static final String PREFS_NAME = "Best2048_Prefs"; // Ajout constante - private static final String HIGH_SCORE_KEY = "high_score"; // Ajout constante - private static final String GAME_STATE_KEY = "game_state"; // Ajout constante - private final Context context; // Ajout contexte - private boolean gameWon = false; // Ajout état - private boolean gameOver = false; // Ajout état + private int[][] board; + private final Random randomNumberGenerator; + private int currentScore = 0; + private int highestScore = 0; // Stocke le HS fourni par MainActivity + private static final int BOARD_SIZE = 4; + private boolean gameWon = false; + private boolean gameOver = false; /** - * Constructeur principal de la classe Game. - * - * @param context Le contexte Android (généralement une Activity). Nécessaire pour accéder aux SharedPreferences. + * Constructeur pour une nouvelle partie. + * Initialise un plateau vide, définit le score à 0 et ajoute deux tuiles initiales. */ - public Game(Context context) { - this.context = context; + public Game() { this.randomNumberGenerator = new Random(); - this.board = new int[BOARD_SIZE][BOARD_SIZE]; - loadHighScore(); - loadGameState(); // Charge l'état précédent ou initialise + // Le highScore sera défini par MainActivity après l'instanciation. + initializeNewBoard(); } /** - * Constructeur pour la restauration d'une partie précédemment sauvegardée. - * - * @param board Le plateau de jeu restauré (l'état des tuiles). - * @param score Le score au moment de la sauvegarde. - * @param highScore Le meilleur score enregistré au moment de la sauvegarde. - * @param context Le contexte Android, nécessaire pour les SharedPreferences. + * Constructeur utilisé lors de la restauration d'une partie sauvegardée. + * @param board Le plateau de jeu restauré. + * @param score Le score courant restauré. */ - public Game(int[][] board, int score, int highScore, Context context) { + public Game(int[][] board, int score) { this.board = board; this.currentScore = score; - this.highestScore = highScore; - this.context = context; this.randomNumberGenerator = new Random(); + // Le highScore sera défini par MainActivity après l'instanciation. + checkWinCondition(); // Recalcule l'état basé sur le plateau chargé + checkGameOverCondition(); } + // --- Getters / Setters --- + /** - * Récupère la valeur d'une cellule spécifique sur le plateau. - * - * @param row L'index de la ligne (0 à BOARD_SIZE - 1). - * @param column L'index de la colonne (0 à BOARD_SIZE - 1). - * @return La valeur de la cellule à la position spécifiée. - * @throws IllegalArgumentException Si row ou column sont en dehors des limites du plateau. + * 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é). */ public int getCellValue(int row, int column) { - if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { - throw new IllegalArgumentException("Indices de ligne ou de colonne hors limites : row=" + row + ", column=" + column); - } + if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { return 0; } return this.board[row][column]; } /** - * Définit la valeur d'une cellule spécifique sur le plateau. - * - * @param row L'index de la ligne (0 à BOARD_SIZE - 1). - * @param col L'index de la colonne (0 à BOARD_SIZE - 1). - * @param value La nouvelle valeur de la cellule. - * @throws IllegalArgumentException Si row ou col sont en dehors des limites du plateau. + * 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. */ public void setCellValue(int row, int col, int value) { - if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { - throw new IllegalArgumentException("Indices de ligne ou de colonne hors limites : row=" + row + ", col=" + col); - } + if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return; } this.board[row][col] = value; } - /** - * Récupère le score actuel de la partie. - * - * @return Le score actuel. - */ - public int getCurrentScore() { - return this.currentScore; - } + /** @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). */ + public int getHighestScore() { return highestScore; } /** - * Met à jour le score actuel. - * - * @param currentScore Le nouveau score actuel. + * 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. */ - public void setCurrentScore(int currentScore) { - this.currentScore = currentScore; - } + public void setHighestScore(int highScore) { this.highestScore = highScore; } - /** - * Récupère le meilleur score enregistré. - * - * @return Le meilleur score. - */ - public int getHighestScore() { - return this.highestScore; - } + /** @return L'état de victoire de la partie (true si >= 2048 atteint). */ + public boolean isGameWon() { return gameWon; } - /** - * Met à jour le meilleur score. Utilisé lors du chargement et après une partie. - * - * @param highestScore Le nouveau meilleur score. - */ - public void setHighestScore(int highestScore) { - this.highestScore = highestScore; - } + public void setGameWon(boolean gameWon) { this.gameWon = gameWon; } - /** - * Met à jour le board. Utilisé lors du chargement. - * - * @param board Le nouveau board. - */ - public void setBoard(int[][] board){ - this.board = board; - } + /** @return L'état de fin de partie (true si aucun mouvement possible). */ + public boolean isGameOver() { return gameOver; } - /** - * Récupère le générateur de nombres aléatoires. - * - * @return Le générateur de nombres aléatoires. - */ - public Random getRandomNumberGenerator() { - return this.randomNumberGenerator; - } + public void setGameOver(boolean gameOver) { this.gameOver = gameOver; } - /** - * Indique si le joueur a gagné la partie (atteint une tuile de 2048). - * - * @return true si le joueur a gagné, false sinon. - */ - public boolean isGameWon() { - return this.gameWon; - } - - /** - * Indique si la partie est terminée (plus de mouvements possibles). - * - * @return true si la partie est terminée, false sinon. - */ - public boolean isGameOver() { - return this.gameOver; - } - - /** - * Définit l'état de la partie gagnée. - * - * @param gameWon true si le joueur a gagné, false sinon. - */ - public void setGameWon(boolean gameWon) { - this.gameWon = gameWon; - } - - /** - * Définit l'état de la partie terminée. - * - * @param gameOver true si la partie est terminée, false sinon. - */ - public void setGameOver(boolean gameOver) { - this.gameOver = gameOver; - } - - /** - * Charge le meilleur score à partir des préférences partagées (SharedPreferences). - */ - public void loadHighScore() { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - setHighestScore(prefs.getInt(HIGH_SCORE_KEY, 0)); - } - - /** - * Enregistre le meilleur score actuel dans les préférences partagées (SharedPreferences). - */ - public void saveHighScore() { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(HIGH_SCORE_KEY, getHighestScore()); - editor.apply(); - } - - /** - * Charge l'état de la partie (plateau, score) à partir des préférences partagées. - */ - private void loadGameState() { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - String savedState = prefs.getString(GAME_STATE_KEY, null); - if (savedState != null) { - Game savedGame = deserialize(savedState, context); - if (savedGame != null) { // Vérifier si la désérialisation a réussi - setBoard(savedGame.board); - setCurrentScore(savedGame.currentScore); - // Le meilleur score est déjà chargé par loadHighScore() appelé dans le constructeur principal - } else { - // Si l'état sauvegardé est invalide, commence une nouvelle partie - initializeNewBoard(); - } - } else { - // Si aucun état n'est sauvegardé, commence une nouvelle partie - initializeNewBoard(); + /** @return Une copie du plateau de jeu actuel (pour la sauvegarde externe). */ + 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<>(); - - // 1. Collecter les coordonnées des cellules vides. 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}); - } + if (board[row][col] == 0) { emptyCells.add(new int[]{row, col}); } } } - - // 2. S'il y a des cellules vides, ajouter une nouvelle tuile. if (!emptyCells.isEmpty()) { - int[] randomCell = emptyCells.get(getRandomNumberGenerator().nextInt(emptyCells.size())); - int row = randomCell[0]; - int col = randomCell[1]; - - int value = generateRandomTileValue(); // Utilise la nouvelle logique de probabilité - setCellValue(row, col, value); + int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size())); + int value = generateRandomTileValue(); + setCellValue(randomCell[0], randomCell[1], value); } } /** - * Génère une valeur de tuile aléatoire selon les probabilités définies. - * - * @return La valeur de la nouvelle tuile (2, 4, 8, ...) avec les probabilités spécifiées. + * 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. */ - private int generateRandomTileValue() { // Logique de probabilité modifiée + private int generateRandomTileValue() { int randomValue = randomNumberGenerator.nextInt(10000); - - if (randomValue < 8540) { // 85.40% - return 2; - } else if (randomValue < 9740) { // 12.00% - return 4; - } else if (randomValue < 9940) { // 2.00% - return 8; - } else if (randomValue < 9990) { // 0.50% - return 16; - } else if (randomValue < 9995) { // 0.05% - return 32; - } else if (randomValue < 9998) { // 0.03% - return 64; - } else if (randomValue < 9999) { // 0.01% - return 128; - }else { // 0.01% - return 256; - } + 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; } /** - * Déplace les tuiles vers le haut, en fusionnant les tuiles de même valeur. - * - * @return True si le plateau a été modifié, False sinon. + * 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. */ - public boolean pushUp() { // Logique refactorisée, retourne boolean, utilise hasMerged - boolean boardChanged = false; - boolean[] hasMerged = new boolean[BOARD_SIZE]; - + 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]; // Réinitialise par colonne + 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; - - // 1. Déplacer la tuile - while (currentRow > 0 && getCellValue(currentRow - 1, col) == 0) { - setCellValue(currentRow - 1, col, currentValue); - setCellValue(currentRow, col, 0); - currentRow--; - boardChanged = true; - } - - // 2. Fusionner si possible + 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); // La tuile d'origine disparaît - setCurrentScore(getCurrentScore() + newValue); - hasMerged[currentRow - 1] = true; - boardChanged = true; - updateHighestScore(); // Met à jour le meilleur score si nécessaire - // checkWinCondition(); // Vérifie si la victoire est atteinte + 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(); // Vérifie après tous les mouvements de la colonne - checkGameOverCondition(); // Vérifie si la partie est terminée - return boardChanged; + } checkWinCondition(); checkGameOverCondition(); return boardChanged; } - /** - * Déplace les tuiles vers le bas, en fusionnant les tuiles de même valeur. - * - * @return True si le plateau a été modifié, False sinon. + * Tente de déplacer et fusionner les tuiles vers le BAS. + * @return true si changement, false sinon. */ - public boolean pushDown() { // Logique refactorisée - boolean boardChanged = false; - boolean[] hasMerged = new boolean[BOARD_SIZE]; - + 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--) { // Itère de bas en haut + for (int row = BOARD_SIZE - 2; row >= 0; row--) { if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); - int currentRow = row; - - // Déplacer vers le bas - while (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == 0) { - setCellValue(currentRow + 1, col, currentValue); - setCellValue(currentRow, col, 0); - currentRow++; - boardChanged = true; - } - - // Fusionner si possible + 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); - setCurrentScore(getCurrentScore() + newValue); - hasMerged[currentRow + 1] = true; - boardChanged = true; - updateHighestScore(); - // checkWinCondition(); + 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; + } checkWinCondition(); checkGameOverCondition(); return boardChanged; } /** - * Déplace les tuiles vers la gauche, en fusionnant les tuiles de même valeur. - * - * @return True si le plateau a été modifié, False sinon. + * Tente de déplacer et fusionner les tuiles vers la GAUCHE. + * @return true si changement, false sinon. */ - public boolean pushLeft() { // Logique refactorisée - boolean boardChanged = false; - boolean[] hasMerged = new boolean[BOARD_SIZE]; // Par ligne cette fois - - for (int row = 0; row < BOARD_SIZE; row++) { - hasMerged = new boolean[BOARD_SIZE]; // Réinitialise par ligne - for (int col = 1; col < BOARD_SIZE; col++) { // Commence à la 2ème colonne - if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); - int currentCol = col; - - // Déplacer vers la gauche - while (currentCol > 0 && getCellValue(row, currentCol - 1) == 0) { - setCellValue(row, currentCol - 1, currentValue); - setCellValue(row, currentCol, 0); - currentCol--; - boardChanged = true; - } - - // Fusionner si possible - 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); - setCurrentScore(getCurrentScore() + newValue); - hasMerged[currentCol - 1] = true; - boardChanged = true; - updateHighestScore(); - // checkWinCondition(); - } - } - } - } - checkWinCondition(); - checkGameOverCondition(); - return boardChanged; - } - - /** - * Déplace les tuiles vers la droite, en fusionnant les tuiles de même valeur. - * - * @return True si le plateau a été modifié, False sinon. - */ - public boolean pushRight() { // Logique refactorisée - boolean boardChanged = false; - boolean[] hasMerged = new boolean[BOARD_SIZE]; // Par ligne - + 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 = BOARD_SIZE - 2; col >= 0; col--) { // Itère de droite à gauche + for (int col = 1; col < BOARD_SIZE; col++) { if (getCellValue(row, col) != 0) { - int currentValue = getCellValue(row, col); - int currentCol = col; - - // Déplacer vers la droite - while (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == 0) { - setCellValue(row, currentCol + 1, currentValue); - setCellValue(row, currentCol, 0); - currentCol++; - boardChanged = true; - } - - // Fusionner si possible - 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); - setCurrentScore(getCurrentScore() + newValue); - hasMerged[currentCol + 1] = true; - boardChanged = true; - updateHighestScore(); - // checkWinCondition(); + 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; + } checkWinCondition(); checkGameOverCondition(); return boardChanged; } - /** - * Met à jour le meilleur score si le score actuel le dépasse, et sauvegarde le nouveau meilleur score. + * Tente de déplacer et fusionner les tuiles vers la DROITE. + * @return true si changement, false sinon. */ - private void updateHighestScore() { // Renommé - if (getCurrentScore() > getHighestScore()) { - setHighestScore(getCurrentScore()); - saveHighScore(); // Sauvegarde implémentée - } + 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--) { + 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; + } + } + } + } checkWinCondition(); checkGameOverCondition(); return boardChanged; } /** - * Sérialise l'état du jeu (plateau, score courant, meilleur score) en une chaîne de caractères. - * Format: "row1col1,row1col2,...,row1colN,row2col1,...,rowNcolN,currentScore,highestScore" - * - * @return Une chaîne représentant l'état complet du jeu. + * Sérialise l'état actuel du jeu (plateau et score courant) en une chaîne. + * Format: "val,val,...,val,score" + * @return Chaîne sérialisée. */ @NonNull @Override - public String toString() { // Renommé et 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(","); // Utilise board directement - } + for (int col = 0; col < BOARD_SIZE; col++) { sb.append(board[row][col]).append(","); } } - - sb.append(getCurrentScore()).append(","); - sb.append(getHighestScore()); // Sérialise highestScore - + sb.append(currentScore); return sb.toString(); } - /** - * Désérialise une chaîne de caractères pour restaurer l'état du jeu. - * - * @param serializedState La chaîne représentant l'état du jeu (au format de la méthode toString). - * @param context Le contexte Android (nécessaire pour le constructeur de Game). - * @return Un nouvel objet Game restauré, ou null si la désérialisation échoue. + * 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. */ - public static Game deserialize(String serializedState, Context context) { // Logique améliorée - if (serializedState == null || serializedState.isEmpty()) { - return null; - } - + public static Game deserialize(String serializedState) { + if (serializedState == null || serializedState.isEmpty()) return null; String[] values = serializedState.split(","); - // Vérifie si le nombre d'éléments correspond à la taille attendue (board + score + highScore) - if (values.length != (BOARD_SIZE * BOARD_SIZE + 2)) { - System.err.println("Erreur de désérialisation : nombre d'éléments incorrect. Attendu=" + (BOARD_SIZE * BOARD_SIZE + 2) + ", Obtenu=" + values.length); - return null; // Longueur incorrecte - } - - int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; - int index = 0; - + if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; + 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++]); - } + for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); } } - - int score = Integer.parseInt(values[index++]); - int highScore = Integer.parseInt(values[index++]); // Désérialise highScore - - // Utilise le constructeur qui prend le contexte - return new Game(newBoard, score, highScore, context); - - } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { - System.err.println("Erreur de désérialisation : " + e.getMessage()); - // e.printStackTrace(); // Optionnel: pour plus de détails dans Logcat - return null; // Erreur de format ou index hors limites - } + int score = Integer.parseInt(values[index]); + return new Game(newBoard, score); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; } } - /** - * Vérifie si la condition de victoire (tuile 2048) est remplie. - * Met à jour la variable gameWon. + * Vérifie si une tuile >= 2048 existe sur le plateau et met à jour l'état `gameWon`. */ - private void checkWinCondition() { // Ajouté - // Ne vérifie que si la partie n'est pas déjà gagnée - if (!isGameWon()) { - for(int row = 0 ; row < BOARD_SIZE; row++){ - for (int col = 0; col < BOARD_SIZE; col++){ - if(getCellValue(row, col) >= 2048){ // Condition >= 2048 - setGameWon(true); - // Optionnel : On pourrait arrêter la vérification ici - // return; - } - } - } + private void checkWinCondition() { + if (!gameWon) { // Optimisation: inutile de revérifier si déjà gagné + for(int r=0; r=2048) { setGameWon(true); return; } } } /** - * Vérifie si la condition de fin de partie (aucun mouvement possible) est remplie. - * Met à jour la variable gameOver. + * Vérifie s'il reste des mouvements possibles (case vide ou fusion adjacente). + * Met à jour l'état `gameOver`. */ - private void checkGameOverCondition() { // Ajouté - // Si une case est vide, la partie n'est pas terminée - if (hasEmptyCell()) { - setGameOver(false); - return; - } - - // Vérifie les fusions possibles horizontalement et verticalement - for (int row = 0; row < BOARD_SIZE; row++) { - for (int col = 0; col < BOARD_SIZE; col++) { - int currentValue = getCellValue(row, col); - - // Vérifie voisin du haut - if (row > 0 && getCellValue(row - 1, col) == currentValue) { - setGameOver(false); - return; - } - // Vérifie voisin du bas - if (row < BOARD_SIZE - 1 && getCellValue(row + 1, col) == currentValue) { - setGameOver(false); - return; - } - // Vérifie voisin de gauche - if (col > 0 && getCellValue(row, col - 1) == currentValue) { - setGameOver(false); - return; - } - // Vérifie voisin de droite - if (col < BOARD_SIZE - 1 && getCellValue(row, col + 1) == currentValue) { - setGameOver(false); - return; - } + 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 game over } /** - * Vérifie s'il existe au moins une cellule vide sur le plateau. - * - * @return true s'il y a une cellule vide, false sinon. + * @return true s'il y a au moins une case vide sur le plateau, false sinon. */ - private boolean hasEmptyCell() { // Ajouté - for (int row = 0; row < BOARD_SIZE; row++) { - for (int col = 0; col < BOARD_SIZE; col++) { - if (getCellValue(row, col) == 0) { - return true; - } - } - } + private boolean hasEmptyCell() { + for(int r=0; rmaxTile) 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 new file mode 100644 index 0000000..3b27aa7 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/GameStats.java @@ -0,0 +1,274 @@ +// 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. +*/ +package legion.muyue.best2048; + +import android.content.Context; +import android.content.SharedPreferences; +import java.util.concurrent.TimeUnit; + +public class GameStats { + + // Clés SharedPreferences (inchangées) + private static final String PREFS_NAME = "Best2048_Prefs"; + private static final String HIGH_SCORE_KEY = "high_score"; + 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"; + private static final String STATS_TOTAL_MERGES = "totalMerges"; + private static final String STATS_HIGHEST_TILE = "highestTile"; + private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached"; + 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"; + 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"; + private static final String STATS_MP_TOTAL_SCORE = "multiplayerTotalScore"; + private static final String STATS_MP_TOTAL_TIME_MS = "multiplayerTotalTimeMs"; + private static final String STATS_MP_LOSSES = "totalMultiplayerLosses"; + private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore"; + + // Champs statistiques (inchangés) + 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 long bestWinningTimeMs; + private long worstWinningTimeMs; + private int multiplayerGamesWon; + private int multiplayerGamesPlayed; + private int multiplayerBestWinningStreak; + private long multiplayerTotalScore; + private long multiplayerTotalTimeMs; + private int totalMultiplayerLosses; + private int multiplayerHighestScore; + private int overallHighScore; + + 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). + */ + public GameStats(Context context) { + this.context = context; + loadStats(); + } + + /** + * Charge toutes les statistiques (générales et multijoueur) et le high score global + * depuis les SharedPreferences. + */ + 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); + 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); + 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); + multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 0); + multiplayerTotalScore = prefs.getLong(STATS_MP_TOTAL_SCORE, 0); + multiplayerTotalTimeMs = prefs.getLong(STATS_MP_TOTAL_TIME_MS, 0); + totalMultiplayerLosses = prefs.getInt(STATS_MP_LOSSES, 0); + multiplayerHighestScore = prefs.getInt(STATS_MP_HIGH_SCORE, 0); + } + + /** + * Sauvegarde toutes les statistiques (générales et multijoueur) et le high score global + * dans les SharedPreferences. + */ + 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(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed); + editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted); + editor.putInt(STATS_TOTAL_MOVES, totalMoves); + editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs); + editor.putInt(STATS_TOTAL_MERGES, totalMerges); + editor.putInt(STATS_HIGHEST_TILE, highestTile); + editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached); + editor.putInt(STATS_PERFECT_GAMES, perfectGames); + editor.putLong(STATS_BEST_WINNING_TIME_MS, bestWinningTimeMs); + editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs); + editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon); + editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed); + editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak); + editor.putLong(STATS_MP_TOTAL_SCORE, multiplayerTotalScore); + editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs); + editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses); + editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore); + editor.apply(); + } + + /** + * 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. + */ + public void startGame() { + totalGamesStarted++; + currentMoves = 0; + mergesThisGame = 0; + currentGameStartTimeMs = System.currentTimeMillis(); + } + + /** + * Enregistre un mouvement effectué pendant la partie en cours. + * Incrémente le compteur de mouvements de la partie et le compteur total. + */ + public void recordMove() { + currentMoves++; + totalMoves++; + } + + /** + * Enregistre une ou plusieurs fusions survenues pendant la partie en cours. + * @param numberOfMerges Le nombre de fusions à ajouter (idéalement précis). + */ + public void recordMerge(int numberOfMerges) { + if (numberOfMerges > 0) { + mergesThisGame += numberOfMerges; + totalMerges += numberOfMerges; + } + } + + /** + * 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. + */ + public void updateHighestTile(int tileValue) { + if (tileValue > this.highestTile) { + this.highestTile = tileValue; + } + } + + /** + * 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. + */ + public void recordWin(long timeTakenMs) { + numberOfTimesObjectiveReached++; + endGame(timeTakenMs); // Finalise les stats de la partie + + if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; } + if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; } + } + + /** + * Enregistre une défaite et finalise les statistiques de la partie. + */ + public void recordLoss() { + 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. + */ + public void endGame(long timeTakenMs) { + totalGamesPlayed++; + addPlayTime(timeTakenMs); + } + + /** + * Ajoute une durée au temps de jeu total cumulé. + * @param durationMs Durée à ajouter en millisecondes. + */ + public void addPlayTime(long durationMs) { + if (durationMs > 0) { + this.totalPlayTimeMs += durationMs; + } + } + + // --- Getters --- + 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 int getMergesThisGame() { return mergesThisGame; } + public int getTotalMerges() { return totalMerges; } + public int getHighestTile() { return highestTile; } + public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; } + public int getPerfectGames() { return perfectGames; } + public long getBestWinningTimeMs() { return bestWinningTimeMs; } + public long getWorstWinningTimeMs() { return worstWinningTimeMs; } + public int getMultiplayerGamesWon() { return multiplayerGamesWon; } + public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; } + public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; } + public long getMultiplayerTotalScore() { return multiplayerTotalScore; } + 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. + */ + 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; + } + + /** + * 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 --- + public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; } + public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; } + public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; } + + /** + * Formate une durée en millisecondes en chaîne "hh:mm:ss" ou "mm:ss". + * @param milliseconds Durée en millisecondes. + * @return Chaîne de temps formatée. + */ + public static String formatTime(long milliseconds) { + long hours = TimeUnit.MILLISECONDS.toHours(milliseconds); + long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; + long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60; + if (hours > 0) { return String.format("%02d:%02d:%02d", hours, minutes, seconds); } + else { return String.format("%02d:%02d", minutes, seconds); } + } +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index 03b9052..b22abb3 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -1,45 +1,38 @@ // Fichier MainActivity.java -// C'est l'activité principale de l'application, l'écran principal avec lequel l'utilisateur interagit. +// Activité principale de l'application 2048, gère l'interface utilisateur et la coordination du jeu. /* Fonctions principales : - - Gère l'interface utilisateur : - - Initialise le GridLayout (gameBoardLayout). - - Met à jour les TextView pour le score (scoreTextView) et le meilleur score (highScoreTextView). - - updateUI() : Redessine la grille en fonction de l'état de l'objet Game. - - setTileAppearance() : Applique le style visuel (couleur, taille du texte) aux tuiles. - - Gère les entrées utilisateur (swipes) via OnSwipeTouchListener. - - Gère les clics sur les boutons (Recommencer, Statistiques, Menu, Multijoueur). - - Affiche des boîtes de dialogue (confirmation de redémarrage, statistiques). - - Gère le cycle de vie de l'activité : - - onCreate() : Initialisation. - - onResume() : Reprise de l'application. - - onPause() : Sauvegarde de l'état et des statistiques. - - Gère la persistance des données (SharedPreferences) : état du jeu, meilleur score, statistiques. - - showStats(), hideStats() : Gère l'affichage/masquage des statistiques (ViewStub). - - Gère les statistiques du jeu. + - 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 : Interagit constamment avec l'objet Game. - - OnSwipeTouchListener : Détection des swipes. - - res/layout/*.xml : Utilise les fichiers de layout pour l'interface utilisateur. - - res/drawable/*.xml : Apparence des tuiles et boutons. - - res/values/*.xml : Couleurs, dimensions, chaînes, styles. - - res/anim/*.xml : Animation du bouton multijoueur. - - SharedPreferences : Sauvegarde et chargement des données. + - 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. */ - package legion.muyue.best2048; -import android.annotation.SuppressLint; // Ajout pour ClickableViewAccessibility +import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.content.Context; // Non utilisé directement mais ok import android.content.SharedPreferences; import android.os.Bundle; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewStub; // Ajout pour ViewStub +import android.view.ViewStub; import android.view.animation.AnimationUtils; import android.widget.TextView; @@ -52,7 +45,7 @@ import android.widget.Button; public class MainActivity extends AppCompatActivity { - // UI Elements (renommés/ajoutés) + // --- UI Elements --- private GridLayout boardGridLayout; private TextView currentScoreTextView; private TextView highestScoreTextView; @@ -60,44 +53,23 @@ public class MainActivity extends AppCompatActivity { private Button multiplayerButton; private Button statisticsButton; private Button menuButton; - private ViewStub statisticsViewStub; // Ajout pour stats + private ViewStub statisticsViewStub; + private View inflatedStatsView; // Référence à la vue des stats une fois gonflée - // Game Logic + // --- Game Logic & Stats --- private Game game; - private static final int BOARD_SIZE = 4; // Ajout constante + private GameStats gameStats; // Instance pour gérer les stats + private static final int BOARD_SIZE = 4; - // Preferences - private SharedPreferences preferences; // Renommé - // Constantes déjà définies dans Game, redéfinies ici ? Ok pour l'instant. + // --- Preferences --- + 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"; - // Statistics (nouveaux champs) private boolean statisticsVisible = false; - private int totalGamesPlayed = 0; - private int totalGamesStarted = 0; - private int totalMoves = 0; - private int currentMoves = 0; - private long totalPlayTimeMs = 0; - private long currentGameStartTimeMs = 0; - private int mergesThisGame = 0; - private int totalMerges = 0; - private int highestTile = 0; - private int numberOfTimesObjectiveReached = 0; - private int perfectGames = 0; - private long averageGameTimeMs = 0; - private long bestWinningTimeMs = Long.MAX_VALUE; - private long worstWinningTimeMs = 0; - // Multiplayer Statistics (nouveaux champs) - private int multiplayerGamesWon = 0; - private int multiplayerGamesPlayed = 0; - private int multiplayerBestWinningStreak = 0; - private int multiplayerAverageScore = 0; - private long multiplayerAverageGameTimeMs = 0; - private int totalMultiplayerLosses = 0; - private int multiplayerHighestScore = 0; + // --- Activity Lifecycle --- @Override protected void onCreate(Bundle savedInstanceState) { @@ -105,241 +77,187 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - findViews(); // Refactorisation onCreate - initializeGame(); // Refactorisation onCreate - setupListeners(); // Refactorisation onCreate + findViews(); // Récupère les vues + initializeGameAndStats(); // Initialise Game, GameStats et charge les données + setupListeners(); // Attache les listeners } - // Getters/Setters ajoutés (pour UI et Game) - public void setBoardGridLayout(GridLayout boardGridLayout) { this.boardGridLayout = boardGridLayout; } - public void setCurrentScoreTextView(TextView currentScoreTextView) { this.currentScoreTextView = currentScoreTextView; } - public void setHighestScoreTextView(TextView highestScoreTextView) { this.highestScoreTextView = highestScoreTextView; } - public void setNewGameButton(Button newGameButton) { this.newGameButton = newGameButton; } - public void setMultiplayerButton(Button multiplayerButton) { this.multiplayerButton = multiplayerButton; } - public void setStatisticsButton(Button statisticsButton) { this.statisticsButton = statisticsButton; } - public void setMenuButton(Button menuButton) { this.menuButton = menuButton; } - public void setStatisticsViewStub(ViewStub statisticsViewStub) { this.statisticsViewStub = statisticsViewStub; } - public void setPreferences(SharedPreferences preferences) { this.preferences = preferences; } - public void setGame(Game game) { this.game = game; } - public Button getNewGameButton() { return this.newGameButton; } - public Button getMultiplayerButton() { return this.multiplayerButton; } - public Button getStatisticsButton() { return this.statisticsButton; } - public Button getMenuButton() { return this.menuButton; } - public GridLayout getBoardGridLayout() { return this.boardGridLayout; } - public Game getGame() { return this.game; } - public TextView getCurrentScoreTextView() { return this.currentScoreTextView; } - public TextView getHighestScoreTextView() { return this.highestScoreTextView; } + @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 + } + // 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 + inflatedStatsView.setVisibility(View.VISIBLE); + multiplayerButton.setVisibility(View.GONE); + } else { + // Si pas encore gonflé (cas rare mais possible), on le fait afficher + toggleStatistics(); + } + } + } + + @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 + } + saveGame(); // Sauvegarde l'état du jeu (plateau + score courant) et le HS + gameStats.saveStats(); // Sauvegarde toutes les stats via GameStats + } + } + + // --- Initialisation --- /** - * Initialise les références aux éléments de l'interface utilisateur (UI). + * Récupère les références des vues du layout principal via leur ID. */ - private void findViews() { // Nouvelle méthode - setBoardGridLayout(findViewById(R.id.gameBoard)); - setCurrentScoreTextView(findViewById(R.id.scoreLabel)); - setHighestScoreTextView(findViewById(R.id.highScoreLabel)); - setNewGameButton(findViewById(R.id.restartButton)); // ID reste restartButton - setStatisticsButton(findViewById(R.id.statsButton)); // ID reste statsButton - setMenuButton(findViewById(R.id.menuButton)); // ID reste menuButton - setMultiplayerButton(findViewById(R.id.multiplayerButton)); - setStatisticsViewStub(findViewById(R.id.statsViewStub)); // Ajout ViewStub + private void findViews() { + boardGridLayout = findViewById(R.id.gameBoard); + currentScoreTextView = findViewById(R.id.scoreLabel); + highestScoreTextView = findViewById(R.id.highScoreLabel); + newGameButton = findViewById(R.id.restartButton); + statisticsButton = findViewById(R.id.statsButton); + menuButton = findViewById(R.id.menuButton); + multiplayerButton = findViewById(R.id.multiplayerButton); + statisticsViewStub = findViewById(R.id.statsViewStub); } /** - * Initialise l'objet Game, charge le meilleur score et restaure l'état de la partie (si disponible). + * Initialise les objets Game et GameStats. + * Charge l'état du jeu sauvegardé (s'il existe) et le meilleur score. + * Met à jour l'interface utilisateur initiale. */ - private void initializeGame() { // Nouvelle méthode - setPreferences(getSharedPreferences(PREFS_NAME, MODE_PRIVATE)); - setGame(new Game(this)); // Passe le contexte à Game - // loadGame(); // Est appelé implicitement par le constructeur de Game via loadGameState - updateUI(); // MAJ initiale de l'UI - loadStatistics(); // Charge les stats - // Initialise le timer pour la partie chargée ou nouvelle - currentGameStartTimeMs = System.currentTimeMillis(); // Démarre timer ici - totalGamesStarted++; // Incrémente car une partie commence (nouvelle ou chargée) + private void initializeGameAndStats() { + preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + gameStats = new GameStats(this); // Crée et charge les stats (y.c. overallHighScore) + loadGame(); // Charge l'état du jeu, crée un nouveau si nécessaire, synchronise le HS + updateUI(); // Affiche l'état chargé ou nouveau + + // Si loadGame a résulté en une nouvelle partie (game==null avant ou deserialize a échoué), + // on s'assure que les stats de la partie en cours sont bien initialisées. + if (game == null || preferences.getString(GAME_STATE_KEY, null) == null) { + startNewGame(); // Assure un état cohérent si aucun jeu n'a été chargé + } else { + // Le timer de la partie chargée sera (re)démarré dans onResume + } } /** - * Configure les listeners pour les interactions de l'utilisateur (boutons, swipes). + * Attache les listeners aux boutons et configure le listener de swipe sur le plateau. */ - private void setupListeners() { // Nouvelle méthode (contenu déplacé) - getNewGameButton().setOnClickListener(v -> { + private void setupListeners() { + newGameButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - // Appelle maintenant showRestartConfirmationDialog au lieu de startNewGame directement showRestartConfirmationDialog(); }); - - getStatisticsButton().setOnClickListener(v -> { + statisticsButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showStatistics(); // Appelle la nouvelle méthode pour les stats + toggleStatistics(); // Affiche/masque les stats }); - - getMenuButton().setOnClickListener(v -> { + menuButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showMenu(); // Appelle le stub de menu + showMenu(); // Affiche dialogue placeholder }); - - getMultiplayerButton().setOnClickListener(v -> { + multiplayerButton.setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showMultiplayerScreen(); // Appelle le stub multijoueur + showMultiplayerScreen(); // Affiche dialogue placeholder }); - - setupSwipeListener(); // Configure le swipe listener + setupSwipeListener(); // Attache le listener de swipe } - /** - * Initialise le GridLayout qui représente le plateau de jeu. Définit le nombre de lignes et colonnes. - */ - private void initGameBoardLayout() { // Inchangé mais utilise getter - getBoardGridLayout().removeAllViews(); - getBoardGridLayout().setColumnCount(BOARD_SIZE); - getBoardGridLayout().setRowCount(BOARD_SIZE); - } + // --- Mise à jour UI --- /** - * Met à jour l'ensemble de l'interface utilisateur : le plateau, le score actuel et le meilleur score. + * Met à jour complètement l'interface utilisateur (plateau et scores). */ - private void updateUI() { // Refactorisé + private void updateUI() { + if (game == null) return; updateBoard(); updateScores(); } /** - * Met à jour l'affichage des tuiles sur le plateau de jeu. + * Redessine le plateau de jeu en créant/mettant à jour les TextViews des tuiles. */ - private void updateBoard() { // Nouvelle méthode (contenu de l'ancien updateUI partie grille) - getBoardGridLayout().removeAllViews(); - + private void updateBoard() { + boardGridLayout.removeAllViews(); for (int row = 0; row < BOARD_SIZE; row++) { for (int col = 0; col < BOARD_SIZE; col++) { TextView tileTextView = new TextView(this); - int value = getGame().getCellValue(row, col); // Utilise getter renommé - - setTileStyle(tileTextView, value); // Utilise méthode renommée - + 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 GridLayout.LayoutParams params = new GridLayout.LayoutParams(); - params.width = 0; - params.height = 0; - params.rowSpec = GridLayout.spec(row, 1f); - params.columnSpec = GridLayout.spec(col, 1f); - + 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 int margin = (int) getResources().getDimension(R.dimen.tile_margin); - params.setMargins(margin, margin, margin, margin); - + params.setMargins(margin, margin, margin, margin); // Applique les marges tileTextView.setLayoutParams(params); - - getBoardGridLayout().addView(tileTextView); + boardGridLayout.addView(tileTextView); // Ajoute la tuile au GridLayout } } } /** - * Met à jour les TextViews du score actuel et du meilleur score. + * Met à jour les TextViews affichant le score courant et le meilleur score. */ - private void updateScores() { // Nouvelle méthode (contenu de l'ancien updateUI partie score) - getCurrentScoreTextView().setText(getString(R.string.score_placeholder, getGame().getCurrentScore())); - getHighestScoreTextView().setText(getString(R.string.high_score_placeholder, getGame().getHighestScore())); + private void updateScores() { + currentScoreTextView.setText(getString(R.string.score_placeholder, game.getCurrentScore())); + highestScoreTextView.setText(getString(R.string.high_score_placeholder, game.getHighestScore())); } - /** - * Définit l'apparence d'une tuile (couleur de fond, couleur du texte, taille du texte) - * en fonction de sa valeur. Utilise un seul Drawable avec des teintes de couleur. - * - * @param tileTextView La TextView représentant la tuile. - * @param value La valeur de la tuile (0, 2, 4, 8, ...). + * Applique le style visuel (fond, texte, taille) à une TextView représentant une tuile. + * @param tileTextView La TextView de la tuile. + * @param value La valeur numérique de la tuile (0 pour vide). */ - private void setTileStyle(TextView tileTextView, int value) { // Renommé et logique couleur texte corrigée - + 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); // Ajout police grasse - - int backgroundColorId; - int textColorId; - int textSizeId; - + tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); + int backgroundColorId; int textColorId; int textSizeId; switch (value) { - case 0: - backgroundColorId = R.color.tile_empty; - textColorId = android.R.color.transparent; - textSizeId = R.dimen.text_size_tile_small; - break; - case 2: - backgroundColorId = R.color.tile_2; - textColorId = R.color.text_tile_low; - textSizeId = R.dimen.text_size_tile_small; - break; - case 4: - backgroundColorId = R.color.tile_4; - textColorId = R.color.text_tile_low; - textSizeId = R.dimen.text_size_tile_small; - break; - case 8: - backgroundColorId = R.color.tile_8; - textColorId = R.color.text_tile_high; // Correction couleur texte - textSizeId = R.dimen.text_size_tile_small; - break; - case 16: - backgroundColorId = R.color.tile_16; - textColorId = R.color.text_tile_high; // Correction - textSizeId = R.dimen.text_size_tile_small; - break; - case 32: - backgroundColorId = R.color.tile_32; - textColorId = R.color.text_tile_high; // Correction - textSizeId = R.dimen.text_size_tile_small; - break; - case 64: - backgroundColorId = R.color.tile_64; - textColorId = R.color.text_tile_high; // Correction - textSizeId = R.dimen.text_size_tile_small; - break; - case 128: - backgroundColorId = R.color.tile_128; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_medium; // Changement taille - break; - case 256: - backgroundColorId = R.color.tile_256; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_medium; - break; - case 512: - backgroundColorId = R.color.tile_512; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_medium; - break; - case 1024: - backgroundColorId = R.color.tile_1024; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_large; // Changement taille - break; - case 2048: - backgroundColorId = R.color.tile_2048; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_large; - break; - default: // > 2048 - backgroundColorId = R.color.tile_super; - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_large; - break; + case 0: backgroundColorId = R.color.tile_empty; textColorId = android.R.color.transparent; textSizeId = R.dimen.text_size_tile_small; break; + case 2: backgroundColorId = R.color.tile_2; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break; + case 4: backgroundColorId = R.color.tile_4; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break; + case 8: backgroundColorId = R.color.tile_8; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break; + case 16: backgroundColorId = R.color.tile_16; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break; + case 32: backgroundColorId = R.color.tile_32; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break; + case 64: backgroundColorId = R.color.tile_64; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_small; break; + case 128: backgroundColorId = R.color.tile_128; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break; + case 256: backgroundColorId = R.color.tile_256; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break; + case 512: backgroundColorId = R.color.tile_512; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break; + case 1024: backgroundColorId = R.color.tile_1024; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break; + case 2048: backgroundColorId = R.color.tile_2048; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break; + default: backgroundColorId = R.color.tile_super; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break; } - - tileTextView.setBackgroundResource(R.drawable.tile_background); // Utilise drawable unique - tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); // Applique teinte - + tileTextView.setBackgroundResource(R.drawable.tile_background); + tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); tileTextView.setTextColor(ContextCompat.getColor(this, textColorId)); tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); } + // --- Gestion des Actions Utilisateur --- + /** - * Configure le listener de swipe sur le GridLayout pour gérer les mouvements des tuiles. + * Configure le listener pour détecter les swipes sur le plateau de jeu. */ - @SuppressLint("ClickableViewAccessibility") // Ajout annotation - private void setupSwipeListener() { // Inchangé mais utilise getter - getBoardGridLayout().setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() { + @SuppressLint("ClickableViewAccessibility") + private void setupSwipeListener() { + boardGridLayout.setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() { @Override public void onSwipeTop() { handleSwipe(Direction.UP); } @Override public void onSwipeBottom() { handleSwipe(Direction.DOWN); } @Override public void onSwipeLeft() { handleSwipe(Direction.LEFT); } @@ -348,371 +266,198 @@ public class MainActivity extends AppCompatActivity { } /** - * Gère un swipe dans une direction donnée, en appelant la méthode appropriée de la classe Game - * et en mettant à jour l'interface utilisateur. - * - * @param direction La direction du swipe (UP, DOWN, LEFT, RIGHT). + * Traite un geste de swipe : appelle la logique de jeu, met à jour les statistiques, + * ajoute une nouvelle tuile, met à jour l'UI et vérifie les conditions de fin de partie. + * @param direction Direction du swipe. */ - private void handleSwipe(Direction direction) { // Logique modifiée pour stats et win/loss - boolean boardChanged; // Vérifie si le plateau a changé - switch (direction) { - case UP: boardChanged = getGame().pushUp(); break; - case DOWN: boardChanged = getGame().pushDown(); break; - case LEFT: boardChanged = getGame().pushLeft(); break; - case RIGHT: boardChanged = getGame().pushRight(); break; + private void handleSwipe(Direction direction) { + if (game == null || gameStats == null || game.isGameOver() || game.isGameWon()) return; + + int scoreBefore = game.getCurrentScore(); + boolean boardChanged; + switch (direction) { /* ... appelle game.pushX() ... */ + case UP: boardChanged = game.pushUp(); break; + case DOWN: boardChanged = game.pushDown(); break; + case LEFT: boardChanged = game.pushLeft(); break; + case RIGHT: boardChanged = game.pushRight(); break; default: boardChanged = false; } if (boardChanged) { - currentMoves++; // Stat - totalMoves++; // Stat - // mergesThisGame++; // Fusion est comptée dans Game ou ici? (pas dans ce snippet) - totalMerges++; // Approximation, devrait être compté par fusion réelle - - // Trouve la tuile la plus haute pour les stats - int currentHighest = 0; - for (int row = 0; row < BOARD_SIZE; row++) { - for (int col = 0; col < BOARD_SIZE; col++) { - if (game.getCellValue(row, col) > currentHighest) { - currentHighest = game.getCellValue(row, col); - } + gameStats.recordMove(); // Met à jour stats mouvement + int scoreAfter = game.getCurrentScore(); + int scoreDelta = scoreAfter - scoreBefore; + if (scoreDelta > 0) { + gameStats.recordMerge(1); // Met à jour stats fusion (simplifié) + // Met à jour le highScore dans Game et GameStats si nécessaire + if (scoreAfter > game.getHighestScore()) { + game.setHighestScore(scoreAfter); + gameStats.setHighestScore(scoreAfter); // Synchronise avec GameStats } } - if (currentHighest > highestTile) { - highestTile = currentHighest; // Met à jour stat highestTile + + gameStats.updateHighestTile(game.getHighestTileValue()); // Met à jour stat tuile max + + game.addNewTile(); // Ajoute tuile + updateUI(); // Met à jour affichage + + // Vérifie victoire/défaite après MAJ UI + if (game.isGameWon()) { + long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); + gameStats.recordWin(timeTaken); + showGameWonDialog(); + } else if (game.isGameOver()) { + long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); + gameStats.recordLoss(); // Appelle endGame implicitement + showGameOverDialog(); } - - - getGame().addNewTile(); // Ajoute une nouvelle tuile - updateUI(); // Met à jour l'affichage - - // Vérifie victoire ou défaite - if (getGame().isGameWon()) { - // Gérer la victoire (par exemple, afficher un message) - if(game.getCellValue(0,0) == 2048){ //Condition spécifique du snippet ? semble être un placeholder - numberOfTimesObjectiveReached++; // Stat - // Gérer les temps de victoire (best/worst) - long timeTaken = System.currentTimeMillis() - currentGameStartTimeMs; - if(timeTaken < bestWinningTimeMs){ - bestWinningTimeMs = timeTaken; - } - if(timeTaken > worstWinningTimeMs){ - worstWinningTimeMs = timeTaken; - } - } - showGameWonDialog(); // Affiche dialog victoire - } else if (getGame().isGameOver()) { - // Gérer la défaite - totalGamesPlayed++; // Stat - showGameOverDialog(); // Affiche dialog défaite - } - } } - - /** - * Énumération représentant les directions possibles de swipe. - */ + // Enum Direction (inchangé) enum Direction { UP, DOWN, LEFT, RIGHT } /** - * Affiche une boîte de dialogue de confirmation avant de recommencer la partie. - * Utilise un layout XML personnalisé (dialog_restart_confirm.xml). + * Affiche la boîte de dialogue demandant confirmation avant de redémarrer. */ - private void showRestartConfirmationDialog() { // Inchangé mais appelle startNewGame + private void showRestartConfirmationDialog() { + // ... (code de la dialog inchangé, appelle startNewGame) ... AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null); builder.setView(dialogView); - - TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle); - TextView dialogMessage = dialogView.findViewById(R.id.dialogMessage); Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton); Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton); - final AlertDialog dialog = builder.create(); - cancelButton.setOnClickListener(v -> dialog.dismiss()); - - confirmButton.setOnClickListener(v -> { - dialog.dismiss(); - startNewGame(); // Appelle la nouvelle méthode - }); - + confirmButton.setOnClickListener(v -> { dialog.dismiss(); startNewGame(); }); dialog.show(); } /** - * Commence une nouvelle partie : réinitialise le jeu et les statistiques de la partie en cours. + * Démarre une nouvelle partie : initialise GameStats pour une nouvelle session, + * crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI. */ - private void startNewGame() { // Nouvelle méthode (anciennement restartGame) + reset stats - totalGamesStarted++; // Stat - currentMoves = 0; // Reset stat partie - mergesThisGame = 0; // Reset stat partie - currentGameStartTimeMs = System.currentTimeMillis(); // Reset timer partie - setGame(new Game(this)); // Crée un nouveau jeu - // game.addNewTile(); // Le constructeur de Game s'en charge maintenant - // game.addNewTile(); - updateUI(); // Met à jour l'affichage + private void startNewGame() { + gameStats.startGame(); // Réinitialise stats partie en cours (mouvements, temps, etc.) + game = new Game(); // Crée un nouveau jeu logique + // Applique le meilleur score global connu (chargé par gameStats) au nouvel objet Game + game.setHighestScore(gameStats.getOverallHighScore()); + updateUI(); // Rafraîchit l'affichage } + // --- Gestion des Statistiques (UI) --- + /** - * Affiche les statistiques du jeu en utilisant un ViewStub pour le chargement différé. + * Affiche ou masque le panneau de statistiques. + * Gonfle le layout via ViewStub si c'est la première fois. */ - private void showStatistics() { // Nouvelle méthode + private void toggleStatistics() { statisticsVisible = !statisticsVisible; - if (statisticsVisible) { - if (statisticsViewStub.getParent() != null) { - statisticsViewStub.inflate(); + if (inflatedStatsView == null) { // Gonfle si pas encore fait + 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 } - updateStatisticsTextViews(); // Met à jour les textes des stats - findViewById(R.id.statistics_layout).setVisibility(View.VISIBLE); // Rend visible le layout gonflé - getMultiplayerButton().setVisibility(View.GONE); // Cache bouton multi + updateStatisticsTextViews(); // Remplit les champs avec les données actuelles + inflatedStatsView.setVisibility(View.VISIBLE); // Affiche + multiplayerButton.setVisibility(View.GONE); // Masque bouton multi } else { - hideStatistics(); // Cache si on reclique - } - } - - /** - * Met à jour tous les TextViews à l'intérieur du layout des statistiques (qui doit déjà être gonflé). - */ - private void updateStatisticsTextViews() { // Nouvelle méthode - View inflatedStatsView = findViewById(R.id.statistics_layout); // Récupère la vue gonflée - if (inflatedStatsView != null) { // Vérifie si la vue existe - 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); - TextView winPercentageLabel = inflatedStatsView.findViewById(R.id.win_percentage_label); - TextView totalPlayTimeLabel = inflatedStatsView.findViewById(R.id.total_play_time_label); - TextView totalMovesLabel = inflatedStatsView.findViewById(R.id.total_moves_label); - TextView currentMovesLabel = inflatedStatsView.findViewById(R.id.current_moves_label); - TextView currentGameTimeLabel = inflatedStatsView.findViewById(R.id.current_game_time_label); - TextView averageGameTimeLabel = inflatedStatsView.findViewById(R.id.average_game_time_label); // Pour solo - TextView bestWinningTimeLabel = inflatedStatsView.findViewById(R.id.best_winning_time_label); - TextView worstWinningTimeLabel = inflatedStatsView.findViewById(R.id.worst_winning_time_label); - TextView totalMergesLabel = inflatedStatsView.findViewById(R.id.total_merges_label); - TextView highestTileLabel = inflatedStatsView.findViewById(R.id.highest_tile_label); - TextView objectiveReachedLabel = inflatedStatsView.findViewById(R.id.number_of_time_objective_reached_label); - TextView perfectGameLabel = inflatedStatsView.findViewById(R.id.perfect_game_label); - TextView multiplayerGamesWonLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_won_label); - TextView multiplayerGamesPlayedLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_played_label); - 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 dupliqué? Utilisons le 2eme - 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 - highScoreStatsLabel.setText(getString(R.string.high_score_stats, game.getHighestScore())); - totalGamesPlayedLabel.setText(getString(R.string.total_games_played, totalGamesPlayed)); - totalGamesStartedLabel.setText(getString(R.string.total_games_started, totalGamesStarted)); - totalMovesLabel.setText(getString(R.string.total_moves, totalMoves)); - currentMovesLabel.setText(getString(R.string.current_moves, currentMoves)); - mergesThisGameLabel.setText(getString(R.string.merges_this_game_label, mergesThisGame)); - totalMergesLabel.setText(getString(R.string.total_merges, totalMerges)); - highestTileLabel.setText(getString(R.string.highest_tile, highestTile)); - objectiveReachedLabel.setText(getString(R.string.number_of_time_objective_reached, numberOfTimesObjectiveReached)); - perfectGameLabel.setText(getString(R.string.perfect_games, perfectGames)); // Il manque la stat perfectGames dans les champs - multiplayerGamesWonLabel.setText(getString(R.string.multiplayer_games_won, multiplayerGamesWon)); - multiplayerGamesPlayedLabel.setText(getString(R.string.multiplayer_games_played, multiplayerGamesPlayed)); - multiplayerBestWinningStreakLabel.setText(getString(R.string.multiplayer_best_winning_streak, multiplayerBestWinningStreak)); - multiplayerAverageScoreLabel.setText(getString(R.string.multiplayer_average_score, multiplayerGamesPlayed > 0 ? (totalMerges / multiplayerGamesPlayed) : 0)); // Calcul approximatif - totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, totalMultiplayerLosses)); - multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, multiplayerHighestScore)); - - // Calculs Pourcentages et Temps - String winPercentage = (totalGamesStarted > 0) ? String.format("%.2f%%", ((double) numberOfTimesObjectiveReached / totalGamesStarted) * 100) : "N/A"; // Utilise numberOfTimesObjectiveReached - winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage)); - - String multiplayerWinRate = (multiplayerGamesPlayed > 0) ? String.format("%.2f%%", ((double) multiplayerGamesWon / multiplayerGamesPlayed) * 100) : "N/A"; - multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate)); - - totalPlayTimeLabel.setText(getString(R.string.total_play_time, formatTime(totalPlayTimeMs))); - // Calcule le temps de la partie en cours dynamiquement si elle n'est pas finie - long currentDuration = (game != null && !game.isGameOver() && !game.isGameWon()) ? System.currentTimeMillis() - currentGameStartTimeMs : 0; - currentGameTimeLabel.setText(getString(R.string.current_game_time, formatTime(currentDuration))); - - long avgTimeSolo = (numberOfTimesObjectiveReached > 0) ? totalPlayTimeMs / numberOfTimesObjectiveReached : 0; // Basé sur victoires? Ou parties jouées? - averageGameTimeLabel.setText(getString(R.string.average_time_per_game, formatTime(avgTimeSolo))); // ID 1 - - long avgTimeMulti = (multiplayerGamesPlayed > 0) ? multiplayerAverageGameTimeMs / multiplayerGamesPlayed : 0; - averageTimePerGameMultiLabel.setText(getString(R.string.average_time_per_game_label, formatTime(avgTimeMulti))); // ID 2 - - bestWinningTimeLabel.setText(getString(R.string.best_winning_time, (bestWinningTimeMs != Long.MAX_VALUE) ? formatTime(bestWinningTimeMs) : "N/A")); - worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (worstWinningTimeMs != 0) ? formatTime(worstWinningTimeMs) : "N/A")); - - // Listener bouton Back dans les stats - Button backButton = inflatedStatsView.findViewById(R.id.backButton); - backButton.setOnClickListener(v -> hideStatistics()); - } - } - - /** - * Masque la vue des statistiques et réaffiche le bouton multijoueur. - */ - private void hideStatistics() { // Nouvelle méthode - View inflatedStatsView = findViewById(R.id.statistics_layout); - if (inflatedStatsView != null) { - inflatedStatsView.setVisibility(View.GONE); - } - getMultiplayerButton().setVisibility(View.VISIBLE); // Réaffiche bouton multi - statisticsVisible = false; // Met à jour l'état - } - - /** - * Charge les statistiques depuis les SharedPreferences. - */ - private void loadStatistics() { // Nouvelle méthode - totalGamesPlayed = preferences.getInt("totalGamesPlayed", 0); - totalGamesStarted = preferences.getInt("totalGamesStarted", 0); - totalMoves = preferences.getInt("totalMoves", 0); - // currentMoves n'est pas chargé, il est spécifique à la session - totalPlayTimeMs = preferences.getLong("totalPlayTimeMs", 0); - // currentGameStartTimeMs est initialisé dans initializeGame ou repris - // mergesThisGame n'est pas chargé - totalMerges = preferences.getInt("totalMerges", 0); - highestTile = preferences.getInt("highestTile", 0); - numberOfTimesObjectiveReached = preferences.getInt("numberOfTimesObjectiveReached", 0); - perfectGames = preferences.getInt("perfectGames", 0); // Chargement stat - // averageGameTimeMs n'est pas chargé, recalculé - bestWinningTimeMs = preferences.getLong("bestWinningTimeMs", Long.MAX_VALUE); - worstWinningTimeMs = preferences.getLong("worstWinningTimeMs", 0); - - multiplayerGamesWon = preferences.getInt("multiplayerGamesWon", 0); - multiplayerGamesPlayed = preferences.getInt("multiplayerGamesPlayed", 0); - multiplayerBestWinningStreak = preferences.getInt("multiplayerBestWinningStreak", 0); - // multiplayerAverageScore recalculé - multiplayerAverageGameTimeMs = preferences.getLong("multiplayerAverageGameTimeMs", 0); - totalMultiplayerLosses = preferences.getInt("totalMultiplayerLosses", 0); - multiplayerHighestScore = preferences.getInt("multiplayerHighScore", 0); - } - - /** - * Sauvegarde les statistiques dans les SharedPreferences. - */ - private void saveStatistics() { // Nouvelle méthode - SharedPreferences.Editor editor = preferences.edit(); - editor.putInt("totalGamesPlayed", totalGamesPlayed); - editor.putInt("totalGamesStarted", totalGamesStarted); - editor.putInt("totalMoves", totalMoves); - // currentMoves non sauvegardé - editor.putLong("totalPlayTimeMs", totalPlayTimeMs); - // currentGameStartTimeMs non sauvegardé directement, utilisé pour calculer totalPlayTimeMs - // mergesThisGame non sauvegardé - editor.putInt("totalMerges", totalMerges); - editor.putInt("highestTile", highestTile); - editor.putInt("numberOfTimesObjectiveReached", numberOfTimesObjectiveReached); - editor.putInt("perfectGames", perfectGames); // Sauvegarde stat - // averageGameTimeMs non sauvegardé - editor.putLong("bestWinningTimeMs", bestWinningTimeMs); - editor.putLong("worstWinningTimeMs", worstWinningTimeMs); - - editor.putInt("multiplayerGamesWon", multiplayerGamesWon); - editor.putInt("multiplayerGamesPlayed", multiplayerGamesPlayed); - editor.putInt("multiplayerBestWinningStreak", multiplayerBestWinningStreak); - // multiplayerAverageScore non sauvegardé - editor.putLong("multiplayerAverageGameTimeMs", multiplayerAverageGameTimeMs); - editor.putInt("totalMultiplayerLosses", totalMultiplayerLosses); - editor.putInt("multiplayerHighScore", multiplayerHighestScore); - - editor.apply(); - } - - /** - * Formate une durée en millisecondes en une chaîne de caractères (hh:mm:ss ou mm:ss). - */ - private String formatTime(long milliseconds) { // Nouvelle méthode - long seconds = (milliseconds / 1000) % 60; - long minutes = (milliseconds / (1000 * 60)) % 60; - long hours = milliseconds / (1000 * 60 * 60); - - if (hours > 0) { - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } else { - return String.format("%02d:%02d", minutes, seconds); - } - } - - /** - * Affiche l'écran du menu (placeholder avec AlertDialog). - */ - private void showMenu() { // Modifié pour utiliser AlertDialog - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Menu") - .setMessage("Fonctionnalité de menu à venir !") - .setPositiveButton("OK", (dialog, id) -> { /* Rien */ }); - AlertDialog dialog = builder.create(); - dialog.show(); - } - - /** - * Affiche l'écran du mode multijoueur (placeholder avec AlertDialog). - */ - private void showMultiplayerScreen() { // Modifié pour utiliser AlertDialog - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("Multijoueur") - .setMessage("Fonctionnalité multijoueur à venir !") - .setPositiveButton("OK", (dialog, id) -> { /* Rien */ }); - AlertDialog dialog = builder.create(); - dialog.show(); - } - - @Override - protected void onResume() { // Ajouté/Modifié - super.onResume(); - // Ne redémarre le chrono que si la partie n'est pas finie - if (game != null && !game.isGameOver() && !game.isGameWon()) { - currentGameStartTimeMs = System.currentTimeMillis(); - } - // Réaffiche les stats si elles étaient visibles - if (statisticsVisible) { - // Assure-toi que la vue est prête avant de mettre à jour - if (findViewById(R.id.statistics_layout) != null) { - updateStatisticsTextViews(); - findViewById(R.id.statistics_layout).setVisibility(View.VISIBLE); - getMultiplayerButton().setVisibility(View.GONE); - } else { - // Si la vue n'est pas encore gonflée, on la montre (ce qui la gonflera et mettra à jour) - showStatistics(); + if (inflatedStatsView != null) { // Masque si la vue existe + inflatedStatsView.setVisibility(View.GONE); } + multiplayerButton.setVisibility(View.VISIBLE); // Réaffiche bouton multi } } - @Override - protected void onPause() { // Modifié pour calculer temps et sauver stats - super.onPause(); - if (game != null) { - // Ajoute le temps écoulé seulement si la partie n'est pas finie - if (!game.isGameOver() && !game.isGameWon()) { - totalPlayTimeMs += System.currentTimeMillis() - currentGameStartTimeMs; - } - saveGame(); // Sauvegarde état jeu - saveStatistics(); // Sauvegarde stats - } - } - - private void saveGame() { - SharedPreferences.Editor editor = preferences.edit(); - editor.putString(GAME_STATE_KEY, game.toString()); - editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); - editor.apply(); - } - /** - * Affiche une boîte de dialogue lorsque le joueur gagne la partie (atteint 2048). + * Remplit les TextViews du panneau de statistiques avec les données de l'objet GameStats. + * Doit être appelé seulement après que `inflatedStatsView` a été initialisé. */ - private void showGameWonDialog() { // Ajouté - // Met à jour les stats de victoire avant d'afficher - numberOfTimesObjectiveReached++; - long timeTaken = System.currentTimeMillis() - currentGameStartTimeMs; - if(timeTaken < bestWinningTimeMs){ bestWinningTimeMs = timeTaken; } - if(timeTaken > worstWinningTimeMs){ worstWinningTimeMs = timeTaken; } + 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); + TextView winPercentageLabel = inflatedStatsView.findViewById(R.id.win_percentage_label); + TextView totalPlayTimeLabel = inflatedStatsView.findViewById(R.id.total_play_time_label); + TextView totalMovesLabel = inflatedStatsView.findViewById(R.id.total_moves_label); + TextView currentMovesLabel = inflatedStatsView.findViewById(R.id.current_moves_label); + TextView currentGameTimeLabel = inflatedStatsView.findViewById(R.id.current_game_time_label); + TextView averageGameTimeLabel = inflatedStatsView.findViewById(R.id.average_game_time_label); + TextView bestWinningTimeLabel = inflatedStatsView.findViewById(R.id.best_winning_time_label); + TextView worstWinningTimeLabel = inflatedStatsView.findViewById(R.id.worst_winning_time_label); + TextView totalMergesLabel = inflatedStatsView.findViewById(R.id.total_merges_label); + TextView highestTileLabel = inflatedStatsView.findViewById(R.id.highest_tile_label); + TextView objectiveReachedLabel = inflatedStatsView.findViewById(R.id.number_of_time_objective_reached_label); + TextView perfectGameLabel = inflatedStatsView.findViewById(R.id.perfect_game_label); + TextView multiplayerGamesWonLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_won_label); + TextView multiplayerGamesPlayedLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_played_label); + 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 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())); + totalMovesLabel.setText(getString(R.string.total_moves, gameStats.getTotalMoves())); + currentMovesLabel.setText(getString(R.string.current_moves, gameStats.getCurrentMoves())); + mergesThisGameLabel.setText(getString(R.string.merges_this_game_label, gameStats.getMergesThisGame())); + totalMergesLabel.setText(getString(R.string.total_merges, gameStats.getTotalMerges())); + highestTileLabel.setText(getString(R.string.highest_tile, gameStats.getHighestTile())); + objectiveReachedLabel.setText(getString(R.string.number_of_time_objective_reached, gameStats.getNumberOfTimesObjectiveReached())); + perfectGameLabel.setText(getString(R.string.perfect_games, gameStats.getPerfectGames())); + multiplayerGamesWonLabel.setText(getString(R.string.multiplayer_games_won, gameStats.getMultiplayerGamesWon())); + multiplayerGamesPlayedLabel.setText(getString(R.string.multiplayer_games_played, gameStats.getMultiplayerGamesPlayed())); + multiplayerBestWinningStreakLabel.setText(getString(R.string.multiplayer_best_winning_streak, gameStats.getMultiplayerBestWinningStreak())); + multiplayerAverageScoreLabel.setText(getString(R.string.multiplayer_average_score, gameStats.getMultiplayerAverageScore())); + 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))); + averageGameTimeLabel.setText(getString(R.string.average_time_per_game, GameStats.formatTime(gameStats.getAverageGameTimeMs()))); + averageTimePerGameMultiLabel.setText(getString(R.string.average_time_per_game_label, GameStats.formatTime(gameStats.getMultiplayerAverageTimeMs()))); + bestWinningTimeLabel.setText(getString(R.string.best_winning_time, (gameStats.getBestWinningTimeMs() != Long.MAX_VALUE) ? GameStats.formatTime(gameStats.getBestWinningTimeMs()) : "N/A")); + 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é) ... */ + 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é) ... */ + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null); + builder.create().show(); + } + + /** Affiche le dialogue de victoire. */ + private void showGameWonDialog() { /* ... (inchangé) ... */ AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Vous avez gagné !") .setMessage("Félicitations ! Vous avez atteint 2048 !") @@ -722,12 +467,8 @@ public class MainActivity extends AppCompatActivity { .show(); } - /** - * Affiche une boîte de dialogue lorsque le joueur perd la partie (aucun mouvement possible). - */ - private void showGameOverDialog() { // Ajouté - totalGamesPlayed++; // Met à jour stat avant d'afficher - + /** Affiche le dialogue de défaite. */ + private void showGameOverDialog() { /* ... (inchangé) ... */ AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Partie terminée !") .setMessage("Aucun mouvement possible. Votre score final est : " + game.getCurrentScore()) @@ -737,4 +478,44 @@ public class MainActivity extends AppCompatActivity { .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 + } else { + editor.remove(GAME_STATE_KEY); // Optionnel: nettoyer si pas de jeu + } + editor.apply(); + } + + /** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */ + private void loadGame() { + String gameState = preferences.getString(GAME_STATE_KEY, null); + int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, gameStats.getOverallHighScore()); // Utilise HS de GameStats comme défaut + + // S'assure que GameStats a la dernière version connue du HS + gameStats.setHighestScore(savedHighScore); + + if (gameState != null) { + game = Game.deserialize(gameState); // Désérialise plateau + score + if (game != null) { + game.setHighestScore(savedHighScore); // Applique le HS à l'objet Game + } else { + // Échec -> Nouvelle partie + game = new Game(); + game.setHighestScore(savedHighScore); + gameStats.startGame(); // Réinitialise stats de partie + } + } else { + // Pas de sauvegarde -> Nouvelle partie + game = new Game(); + game.setHighestScore(savedHighScore); + gameStats.startGame(); // Réinitialise stats de partie + } + } + } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java index caf3a09..082408f 100644 --- a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java +++ b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java @@ -143,7 +143,6 @@ public class OnSwipeTouchListener implements View.OnTouchListener { } } } catch (Exception exception) { - // exception.printStackTrace(); // Commenté dans le nouveau code ? Gardons commenté. exception.fillInStackTrace(); // Gestion des erreurs (journalisation). } return result;