- * NOTE : Ce format est simple mais peut être fragile si la structure du jeu - * ou les données à sauvegarder évoluent. Un format comme JSON pourrait offrir plus de flexibilité. - *
+ * Fournit une représentation textuelle simple de l'état du jeu (plateau + score). + * Le format est une chaîne de valeurs séparées par des virgules : + * toutes les valeurs du plateau (ligne par ligne), suivies du score actuel. + * Utilisé pour la sérialisation simple via {@link #deserialize(String)}. * - * @return Une chaîne non nulle représentant l'état sérialisé du jeu. + * @return Une chaîne représentant l'état du jeu. Ne sera jamais null. */ @NonNull @Override @@ -456,149 +449,139 @@ public class Game { sb.append(board[row][col]).append(","); } } - // Ajoute le score à la fin, séparé par une virgule + // Ajoute le score à la fin, sans virgule après sb.append(currentScore); return sb.toString(); } /** - * Crée (désérialise) un objet {@code Game} à partir de sa représentation sous forme de chaîne, - * telle que générée par {@link #toString()}. - * Le meilleur score (`highestScore`) n'est pas inclus dans la chaîne et doit être - * défini séparément sur l'objet retourné via {@link #setHighestScore(int)}. - *- * NOTE : Cette méthode dépend du format spécifique généré par {@code toString()}. - *
+ * Crée une instance de {@link Game} à partir d'une chaîne sérialisée. + * La chaîne doit correspondre au format généré par {@link #toString()}. * - * @param serializedState La chaîne représentant l'état du jeu (plateau + score courant). - * @return Une nouvelle instance de {@code Game} si la désérialisation réussit, - * ou {@code null} si la chaîne est invalide, vide, ou a un format incorrect - * (mauvais nombre d'éléments, valeur non entière). + * @param serializedState La chaîne contenant l'état du jeu sérialisé. Peut être null ou vide. + * @return Une nouvelle instance de {@link Game} initialisée avec l'état désérialisé, + * ou {@code null} si la chaîne est invalide, vide, null, ou ne correspond pas au format attendu. */ @Nullable public static Game deserialize(@Nullable String serializedState) { if (serializedState == null || serializedState.isEmpty()) { - return null; + return null; // Chaîne vide ou nulle } String[] values = serializedState.split(","); - // Vérifie si le nombre d'éléments correspond à la taille du plateau + 1 (pour le score) + // Vérifie si le nombre de valeurs correspond à (taille*taille + 1 score) if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) { - return null; + return null; // Nombre incorrect de valeurs } int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0; try { - // Remplit le plateau à partir des valeurs de la chaîne + // Remplit le nouveau plateau for (int row = 0; row < BOARD_SIZE; row++) { for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); } } - // Le dernier élément est le score - int score = Integer.parseInt(values[index]); - - // Crée une nouvelle instance de Game avec les données désérialisées - // Le constructeur recalculera gameWon et gameOver. + // Récupère le score + int score = Integer.parseInt(values[index]); // La dernière valeur est le score + // Crée et retourne le nouvel objet Game en utilisant le constructeur approprié return new Game(newBoard, score); - } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) { - // En cas d'erreur de format (valeur non entière, problème d'indice, ou dimension plateau incorrecte dans constructeur) - // Log l'erreur pourrait être utile pour le débogage - System.err.println("Erreur lors de la désérialisation de l'état du jeu: " + e.getMessage()); - return null; + return null; // Échec de la désérialisation } } /** - * Vérifie si la condition de victoire (au moins une tuile avec une valeur >= 2048) - * est actuellement atteinte sur le plateau. Met à jour l'état interne `gameWon`. - * Cette vérification s'arrête dès qu'une tuile gagnante est trouvée ou si le jeu - * est déjà marqué comme gagné. + * Vérifie si la condition de victoire (une tuile >= 2048) est atteinte. + * Met à jour le flag {@link #gameWon} si nécessaire. + * Ne fait rien si le jeu est déjà marqué comme gagné. */ private void checkWinCondition() { - // Si le jeu est déjà marqué comme gagné, pas besoin de revérifier + // Si déjà gagné, pas besoin de revérifier if (gameWon) { return; } - // Parcours le plateau à la recherche d'une tuile >= 2048 + // Parcourt le plateau à la recherche d'une tuile >= 2048 for (int r = 0; r < BOARD_SIZE; r++) { for (int c = 0; c < BOARD_SIZE; c++) { - if (getCellValue(r, c) >= 2048) { - setGameWon(true); // Met à jour l'état interne - return; // Sort dès qu'une condition de victoire est trouvée + // Utiliser getCellValue pour la cohérence (bien que l'accès direct soit possible ici) + if (getCellValue(r, c) >= 2048) { // Utiliser une constante WINNING_TILE = 2048 + setGameWon(true); + // Pas besoin de continuer à chercher une fois la condition atteinte + return; } } } - // Si la boucle se termine sans trouver de tuile >= 2048, gameWon reste false (ou son état précédent) + // Si on arrive ici, aucune tuile >= 2048 n'a été trouvée } /** - * Vérifie si la condition de fin de partie (Game Over) est atteinte. - * Cela se produit si le plateau est plein (pas de case vide) ET si aucune - * fusion n'est possible entre des tuiles adjacentes (horizontalement ou verticalement). - * Met à jour l'état interne `gameOver`. + * Vérifie si la condition de fin de partie est atteinte (plus aucun mouvement possible). + * Un jeu est terminé s'il n'y a plus de cellules vides ET aucune paire de tuiles + * adjacentes identiques (horizontalement ou verticalement). + * Met à jour le flag {@link #gameOver} si nécessaire. + * Ne fait rien si le jeu est déjà marqué comme terminé. */ private void checkGameOverCondition() { - // Si le jeu est déjà terminé, pas besoin de revérifier + // Si déjà terminé, pas besoin de revérifier if (gameOver) { return; } - // Si il y a au moins une case vide, le jeu ne peut pas être terminé + + // S'il y a au moins une cellule vide, le jeu n'est pas terminé if (hasEmptyCell()) { - setGameOver(false); + setGameOver(false); // Assure que le flag est bien à false return; } - // Le plateau est plein. Vérifier s'il existe au moins une fusion possible. + // S'il n'y a pas de cellules vides, vérifie les fusions possibles + // Parcourt toutes les cellules for (int r = 0; r < BOARD_SIZE; r++) { for (int c = 0; c < BOARD_SIZE; c++) { int current = getCellValue(r, c); - // Vérifie le voisin du HAUT (si existe) + // Vérifie la fusion possible vers le haut (si pas sur la première ligne) if (r > 0 && getCellValue(r - 1, c) == current) { - setGameOver(false); return; // Fusion possible vers le haut + setGameOver(false); return; // Mouvement possible trouvé } - // Vérifie le voisin du BAS (si existe) + // Vérifie la fusion possible vers le bas (si pas sur la dernière ligne) if (r < BOARD_SIZE - 1 && getCellValue(r + 1, c) == current) { - setGameOver(false); return; // Fusion possible vers le bas + setGameOver(false); return; // Mouvement possible trouvé } - // Vérifie le voisin de GAUCHE (si existe) + // Vérifie la fusion possible vers la gauche (si pas sur la première colonne) if (c > 0 && getCellValue(r, c - 1) == current) { - setGameOver(false); return; // Fusion possible vers la gauche + setGameOver(false); return; // Mouvement possible trouvé } - // Vérifie le voisin de DROITE (si existe) + // Vérifie la fusion possible vers la droite (si pas sur la dernière colonne) if (c < BOARD_SIZE - 1 && getCellValue(r, c + 1) == current) { - setGameOver(false); return; // Fusion possible vers la droite + setGameOver(false); return; // Mouvement possible trouvé } } } - // Si on arrive ici, le plateau est plein ET aucune fusion adjacente n'est possible. + // Si on arrive ici, il n'y a pas de cellules vides ET aucune fusion possible setGameOver(true); } /** - * Vérifie rapidement si le plateau contient au moins une case vide (valeur 0). - * Utilisé principalement par {@link #checkGameOverCondition()}. + * Vérifie rapidement s'il existe au moins une cellule vide sur le plateau. * - * @return true s'il existe au moins une case vide, false si le plateau est plein. + * @return {@code true} s'il y a au moins une cellule avec la valeur 0, {@code false} sinon. */ private boolean hasEmptyCell() { for (int r = 0; r < BOARD_SIZE; r++) { for (int c = 0; c < BOARD_SIZE; c++) { if (getCellValue(r, c) == 0) { - return true; // Sort dès qu'une case vide est trouvée + return true; // Trouvé une cellule vide } } } - return false; // Aucune case vide trouvée après avoir parcouru tout le plateau + return false; // Aucune cellule vide trouvée } /** - * Retourne la valeur de la plus haute tuile (la plus grande valeur numérique) - * actuellement présente sur le plateau de jeu. + * Calcule et retourne la valeur de la plus haute tuile actuellement sur le plateau. * - * @return La valeur maximale trouvée sur le plateau, ou 0 si le plateau est vide. + * @return La valeur maximale parmi toutes les tuiles du plateau. Retourne 0 si le plateau est vide. */ public int getHighestTileValue() { int maxTile = 0; diff --git a/app/src/main/java/legion/muyue/best2048/GameStats.java b/app/src/main/java/legion/muyue/best2048/GameStats.java index 843e251..4363d54 100644 --- a/app/src/main/java/legion/muyue/best2048/GameStats.java +++ b/app/src/main/java/legion/muyue/best2048/GameStats.java @@ -1,9 +1,3 @@ -/** - * Gère la collecte, la persistance (via {@link SharedPreferences}) et l'accès - * aux statistiques du jeu 2048. - * Inclut des statistiques générales, solo, et des placeholders pour le multijoueur. - * Charge et sauvegarde automatiquement les statistiques via SharedPreferences. - */ package legion.muyue.best2048; import android.annotation.SuppressLint; @@ -11,122 +5,130 @@ import android.content.Context; import android.content.SharedPreferences; import java.util.concurrent.TimeUnit; +/** + * Gère le suivi, le stockage et la récupération des statistiques de jeu + * pour les modes solo et multijoueur du jeu Best2048. + * Utilise {@link SharedPreferences} pour la persistance des données. + * Nécessite un {@link Context} Android pour l'initialisation. + * + * Cette classe suit diverses métriques telles que le nombre de parties jouées, + * les scores, le temps de jeu, les mouvements, les fusions, les séries de victoires, etc. + */ public class GameStats { // --- Constantes pour SharedPreferences --- - - /** Nom du fichier de préférences partagées utilisé pour stocker les statistiques et l'état du jeu. */ + /** Nom du fichier de préférences partagées utilisé pour stocker les statistiques. */ private static final String PREFS_NAME = "Best2048_Prefs"; - /** - * Clé pour le meilleur score global. - * NOTE : Cette clé est également utilisée directement par MainActivity/Game pour la sauvegarde/chargement - * de l'état du jeu. La synchronisation est gérée entre les classes. - */ + /** Clé pour stocker le meilleur score global (principalement solo). */ private static final String HIGH_SCORE_KEY = "high_score"; - - // Clés spécifiques aux statistiques persistantes + // Clés pour les statistiques Solo + /** Clé pour le nombre total de parties solo terminées (victoire ou défaite). */ private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed"; + /** Clé pour le nombre total de parties solo démarrées. */ private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted"; + /** Clé pour le nombre total de mouvements effectués en mode solo. */ private static final String STATS_TOTAL_MOVES = "totalMoves"; + /** Clé pour le temps de jeu total cumulé en mode solo (en millisecondes). */ private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs"; + /** Clé pour le nombre total de fusions de tuiles en mode solo. */ private static final String STATS_TOTAL_MERGES = "totalMerges"; + /** Clé pour la valeur de la plus haute tuile jamais atteinte en mode solo. */ private static final String STATS_HIGHEST_TILE = "highestTile"; + /** Clé pour le nombre de fois où l'objectif (ex: tuile 2048) a été atteint en mode solo. */ private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached"; - // private static final String STATS_PERFECT_GAMES = "perfectGames"; // Clé supprimée car concept non défini + /** Clé pour le meilleur temps pour atteindre l'objectif en mode solo (en millisecondes). */ private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs"; + /** Clé pour le pire (plus long) temps pour atteindre l'objectif en mode solo (en millisecondes). */ private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs"; - - // Clés pour les statistiques multijoueur (fonctionnalité future) + // Clés pour les statistiques Multijoueur (persistées) + /** Clé pour le nombre total de parties multijoueur gagnées. */ private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon"; + /** Clé pour le nombre total de parties multijoueur jouées (terminées). */ private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed"; + /** Clé pour la meilleure série de victoires consécutives en multijoueur. */ private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak"; + /** Clé pour le score total cumulé en multijoueur. */ private static final String STATS_MP_TOTAL_SCORE = "multiplayerTotalScore"; + /** Clé pour le temps de jeu total cumulé en multijoueur (en millisecondes). */ private static final String STATS_MP_TOTAL_TIME_MS = "multiplayerTotalTimeMs"; + /** Clé pour le nombre total de parties multijoueur perdues. */ private static final String STATS_MP_LOSSES = "totalMultiplayerLosses"; + /** Clé pour le meilleur score obtenu dans une seule partie multijoueur. */ private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore"; - - // --- Champs de Statistiques --- - - // Générales & Solo - /** Nombre total de parties terminées (victoire ou défaite). */ + // --- Champs Solo & Général (persistés via SharedPreferences) --- + /** Nombre total de parties solo terminées (victoires + défaites). Persisté. */ private int totalGamesPlayed; - /** Nombre total de parties démarrées (via bouton "Nouvelle Partie"). */ + /** Nombre total de parties solo commencées. Persisté. */ private int totalGamesStarted; - /** Nombre total de mouvements (swipes valides) effectués dans toutes les parties. */ + /** Nombre total de mouvements effectués dans toutes les parties solo. Persisté. */ private int totalMoves; - /** Temps total passé à jouer (en millisecondes) sur toutes les parties. */ + /** Temps de jeu total cumulé pour toutes les parties solo (en millisecondes). Persisté. */ private long totalPlayTimeMs; - /** Nombre total de fusions de tuiles effectuées dans toutes les parties. */ + /** Nombre total de fusions effectuées dans toutes les parties solo. Persisté. */ private int totalMerges; - /** Valeur de la plus haute tuile atteinte globalement dans toutes les parties. */ + /** Valeur de la plus haute tuile jamais atteinte en mode solo. Persisté. */ private int highestTile; - /** Nombre de fois où l'objectif (atteindre 2048 ou plus) a été atteint. */ + /** Nombre de fois où l'objectif (ex: 2048) a été atteint en solo. Persisté. */ private int numberOfTimesObjectiveReached; - // private int perfectGames; // Champ supprimé car concept non défini - /** Meilleur temps (le plus court, en ms) pour atteindre l'objectif (>= 2048). */ + /** Meilleur temps (le plus court) pour atteindre l'objectif en solo (en millisecondes). Persisté. */ private long bestWinningTimeMs; - /** Pire temps (le plus long, en ms) pour atteindre l'objectif (>= 2048). */ + /** Pire temps (le plus long) pour atteindre l'objectif en solo (en millisecondes). Persisté. */ private long worstWinningTimeMs; - /** - * Meilleur score global atteint. Persisté via HIGH_SCORE_KEY. - * Synchronisé avec MainActivity/Game lors du chargement/sauvegarde. - */ + /** Meilleur score global atteint (principalement en mode solo). Persisté. */ private int overallHighScore; - // Partie en cours (non persistées telles quelles, utilisées pour calculs en temps réel) - /** Nombre de mouvements effectués dans la partie actuelle. */ + // --- Champs Partie en cours (Solo - transitoires, non persistés) --- + /** Nombre de mouvements effectués dans la partie solo actuelle. Non persisté. */ private int currentMoves; - /** Timestamp (en ms) du début de la partie actuelle. Réinitialisé à chaque nouvelle partie ou reprise. */ + /** Timestamp (millisecondes) du début de la partie solo actuelle. Non persisté. */ private long currentGameStartTimeMs; - /** Nombre de fusions effectuées dans la partie actuelle. */ + /** Nombre de fusions effectuées dans la partie solo actuelle. Non persisté. */ private int mergesThisGame; - // Multijoueur (placeholders pour fonctionnalité future) - /** Nombre de parties multijoueur gagnées. */ + // --- Champs Multijoueur (persistés via SharedPreferences) --- + /** Nombre total de parties multijoueur gagnées. Persisté. */ private int multiplayerGamesWon; - /** Nombre de parties multijoueur jouées (terminées). */ + /** Nombre total de parties multijoueur jouées (terminées). Persisté. */ private int multiplayerGamesPlayed; - /** Plus longue série de victoires consécutives en multijoueur. */ + /** Meilleure série de victoires consécutives en multijoueur jamais atteinte. Persisté. */ private int multiplayerBestWinningStreak; - /** Score total accumulé dans toutes les parties multijoueur. */ + /** Score total cumulé sur toutes les parties multijoueur. Persisté. */ private long multiplayerTotalScore; - /** Temps total passé dans les parties multijoueur (en ms). */ + /** Temps de jeu total cumulé pour toutes les parties multijoueur (en millisecondes). Persisté. */ private long multiplayerTotalTimeMs; - /** Nombre total de défaites en multijoueur. */ + /** Nombre total de parties multijoueur perdues. Persisté. */ private int totalMultiplayerLosses; - /** Meilleur score atteint dans une seule partie multijoueur. */ + /** Meilleur score obtenu dans une seule partie multijoueur. Persisté. */ private int multiplayerHighestScore; - /** Contexte applicatif nécessaire pour accéder aux SharedPreferences. */ + // --- Champs Transitoires (non persistés) --- + /** Série de victoires consécutives actuelle en multijoueur. Non persisté. */ + private int currentMultiplayerWinningStreak = 0; + + /** Contexte Android nécessaire pour accéder aux SharedPreferences. */ private final Context context; + // --- Constructeur & Persistance --- + /** - * Constructeur de GameStats. - * Initialise l'objet et charge immédiatement les statistiques persistantes - * depuis les SharedPreferences via {@link #loadStats()}. + * Construit une nouvelle instance de GameStats. + * Charge immédiatement les statistiques persistées depuis les SharedPreferences. * - * @param context Contexte de l'application (Activity ou Application). Utilise `getApplicationContext()` pour éviter les fuites de mémoire. + * @param context Le contexte de l'application Android, nécessaire pour accéder aux SharedPreferences. Ne doit pas être null. */ public GameStats(Context context) { - this.context = context.getApplicationContext(); // Important: utiliser le contexte applicatif - loadStats(); // Charge les statistiques dès l'initialisation + this.context = context.getApplicationContext(); // Utilise le contexte d'application pour éviter les fuites de mémoire + loadStats(); // Charge les statistiques au moment de la création } - // --- Persistance (SharedPreferences) --- - /** - * Charge toutes les statistiques persistantes depuis le fichier SharedPreferences défini par {@link #PREFS_NAME}. - * Si une clé n'existe pas, une valeur par défaut est utilisée (0, Long.MAX_VALUE pour best time, etc.). - * Appelé automatiquement par le constructeur. + * Charge toutes les statistiques persistées depuis le fichier SharedPreferences défini par {@link #PREFS_NAME}. + * Si une clé n'est pas trouvée, une valeur par défaut est utilisée (généralement 0 ou Long.MAX_VALUE pour best time). */ public void loadStats() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - - // Charge le high score (clé partagée) overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0); - - // Charge les statistiques générales et solo totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0); totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0); totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0); @@ -134,11 +136,9 @@ public class GameStats { 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); // Supprimé - bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme indicateur "pas encore de temps enregistré" - worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0L); - - // Charge les statistiques multijoueur (placeholders) + bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); + worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0L); // 0 est ok pour le pire temps + // Chargement des stats Multi 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); @@ -149,19 +149,14 @@ public class GameStats { } /** - * Sauvegarde toutes les statistiques persistantes actuelles dans le fichier SharedPreferences. - * Utilise `apply()` pour une sauvegarde asynchrone et non bloquante. - * Devrait être appelée lorsque l'application se met en pause ou est détruite, - * ou après une mise à jour significative des statistiques (ex: reset). + * Sauvegarde l'état actuel de toutes les statistiques persistables dans le fichier SharedPreferences. + * Utilise {@link SharedPreferences.Editor#apply()} pour une sauvegarde asynchrone en arrière-plan. + * Les statistiques transitoires (partie en cours) ne sont pas sauvegardées. */ public void saveStats() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); - - // Sauvegarde le high score (clé partagée) editor.putInt(HIGH_SCORE_KEY, overallHighScore); - - // Sauvegarde les statistiques générales et solo editor.putInt(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed); editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted); editor.putInt(STATS_TOTAL_MOVES, totalMoves); @@ -169,11 +164,9 @@ public class GameStats { 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); // Supprimé editor.putLong(STATS_BEST_WINNING_TIME_MS, bestWinningTimeMs); editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs); - - // Sauvegarde les statistiques multijoueur (placeholders) + // Sauvegarde des stats Multi editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon); editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed); editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak); @@ -181,29 +174,26 @@ public class GameStats { editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs); editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses); editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore); - - // Applique les changements de manière asynchrone - editor.apply(); + editor.apply(); // Sauvegarde asynchrone } - // --- Méthodes de Mise à Jour des Statistiques --- + // --- Méthodes de suivi pour le mode Solo --- /** - * Doit être appelée au début de chaque nouvelle partie (ex: clic sur "Nouvelle Partie"). - * Incrémente le compteur de parties démarrées (`totalGamesStarted`) et réinitialise - * les statistiques spécifiques à la partie en cours (`currentMoves`, `mergesThisGame`, `currentGameStartTimeMs`). + * Enregistre le début d'une nouvelle partie solo. + * Incrémente le compteur de parties démarrées, réinitialise les compteurs de la partie actuelle + * (mouvements, fusions) et enregistre l'heure de début. */ public void startGame() { totalGamesStarted++; currentMoves = 0; mergesThisGame = 0; - currentGameStartTimeMs = System.currentTimeMillis(); // Enregistre le temps de début + currentGameStartTimeMs = System.currentTimeMillis(); // Enregistre l'heure de début } /** - * Enregistre un mouvement réussi (un swipe qui a modifié l'état du plateau). - * Incrémente le compteur de mouvements pour la partie en cours (`currentMoves`) - * et le compteur total de mouvements (`totalMoves`). + * Enregistre un mouvement effectué dans la partie solo actuelle. + * Incrémente le compteur de mouvements pour la partie en cours et le compteur total de mouvements. */ public void recordMove() { currentMoves++; @@ -211,11 +201,10 @@ public class GameStats { } /** - * Enregistre une ou plusieurs fusions survenues lors d'un mouvement. - * Incrémente le compteur de fusions pour la partie en cours (`mergesThisGame`) - * et le compteur total de fusions (`totalMerges`). + * Enregistre une ou plusieurs fusions effectuées lors d'un mouvement en solo. + * Incrémente le compteur de fusions pour la partie en cours et le compteur total de fusions. * - * @param numberOfMerges Le nombre de fusions distinctes réalisées lors du dernier mouvement (typiquement 1 ou plus). + * @param numberOfMerges Le nombre de fusions à ajouter (doit être > 0). */ public void recordMerge(int numberOfMerges) { if (numberOfMerges > 0) { @@ -225,86 +214,144 @@ public class GameStats { } /** - * Met à jour la statistique de la plus haute tuile atteinte globalement (`highestTile`), - * si la valeur fournie est supérieure à la valeur actuelle enregistrée. - * Devrait être appelée après chaque mouvement qui pourrait créer une nouvelle tuile max. + * Met à jour la valeur de la plus haute tuile jamais atteinte si la nouvelle valeur est supérieure. * - * @param currentHighestTileValue La valeur de la tuile la plus haute sur le plateau à l'instant T. + * @param tileValue La valeur de la tuile potentiellement la plus haute. */ - public void updateHighestTile(int currentHighestTileValue) { - if (currentHighestTileValue > this.highestTile) { - this.highestTile = currentHighestTileValue; - // La sauvegarde se fera via saveStats() globalement + public void updateHighestTile(int tileValue) { + if (tileValue > highestTile) { + highestTile = tileValue; + saveStats(); } } /** - * Enregistre une victoire (atteinte de l'objectif >= 2048). - * Incrémente le compteur de victoires (`numberOfTimesObjectiveReached`) et met à jour - * les statistiques de meilleur/pire temps de victoire si nécessaire. - * Appelle également {@link #endGame(long)} pour finaliser les statistiques générales de la partie. + * Enregistre une victoire en mode solo (atteinte de l'objectif). + * Incrémente le compteur de victoires, met à jour les meilleurs/pires temps de victoire si applicable, + * et appelle {@link #endGame(long)} pour finaliser les statistiques de la partie. * - * @param timeTakenMs Temps écoulé (en millisecondes) pour terminer cette partie gagnante. + * @param timeMs Le temps pris pour gagner cette partie (en millisecondes). */ - public void recordWin(long timeTakenMs) { + public void recordWin(long timeMs) { numberOfTimesObjectiveReached++; - if (timeTakenMs < bestWinningTimeMs) { - bestWinningTimeMs = timeTakenMs; + if (timeMs < bestWinningTimeMs) { + bestWinningTimeMs = timeMs; } - // Utiliser >= pour le pire temps permet de l'enregistrer même si c'est le premier - if (timeTakenMs >= worstWinningTimeMs) { - worstWinningTimeMs = timeTakenMs; + // Utilise >= pour worstWinningTimeMs pour s'assurer qu'il est mis à jour même si le premier temps est 0 + if (timeMs >= worstWinningTimeMs) { + worstWinningTimeMs = timeMs; } - endGame(timeTakenMs); // Finalise aussi le temps total et le compteur de parties jouées + endGame(timeMs); // Finalise la partie avec le temps enregistré } /** - * Enregistre une défaite (Game Over - plateau plein sans mouvement possible). - * Calcule le temps écoulé pour la partie et appelle {@link #endGame(long)} - * pour finaliser les statistiques générales. + * Enregistre une défaite en mode solo. + * Calcule le temps de jeu pour cette partie et appelle {@link #endGame(long)}. */ public void recordLoss() { - // Calcule le temps écoulé pour cette partie avant de la finaliser - long timeTakenMs = System.currentTimeMillis() - currentGameStartTimeMs; - endGame(timeTakenMs); + long timeMs = (currentGameStartTimeMs > 0) ? System.currentTimeMillis() - currentGameStartTimeMs : 0; + endGame(timeMs); // Finalise la partie } /** - * Finalise les statistiques générales à la fin d'une partie (victoire ou défaite). - * Incrémente le compteur de parties jouées (`totalGamesPlayed`) et ajoute le temps - * de la partie terminée au temps de jeu total (`totalPlayTimeMs`). + * Méthode privée pour finaliser les statistiques communes à la fin d'une partie solo (victoire ou défaite). + * Incrémente le compteur total de parties jouées et ajoute le temps de jeu de cette partie. * - * @param timeTakenMs Temps total (en millisecondes) de la partie qui vient de se terminer. + * @param timeMs Le temps de jeu pour la partie qui vient de se terminer (en millisecondes). */ - private void endGame(long timeTakenMs) { + private void endGame(long timeMs) { totalGamesPlayed++; - addPlayTime(timeTakenMs); // Ajoute la durée de la partie terminée au total - // Ne pas réinitialiser currentGameStartTimeMs ici, car une nouvelle partie - // pourrait ne pas commencer immédiatement (ex: affichage dialogue Game Over). - // startGame() s'en chargera. + addPlayTime(timeMs); + currentGameStartTimeMs = 0; + saveStats(); } /** - * Ajoute une durée (en millisecondes) au temps de jeu total enregistré (`totalPlayTimeMs`). - * Typiquement appelé dans `onPause` de l'activité pour enregistrer le temps écoulé - * depuis `onResume` ou `startGame`. + * Ajoute une durée au temps de jeu total cumulé en mode solo. * - * @param durationMs Durée positive (en millisecondes) à ajouter au temps total. + * @param durationMs La durée à ajouter (en millisecondes, doit être > 0). */ public void addPlayTime(long durationMs) { if (durationMs > 0) { - this.totalPlayTimeMs += durationMs; + totalPlayTimeMs += durationMs; } } + // --- Méthodes de suivi pour le mode Multijoueur --- + /** - * Réinitialise toutes les statistiques (générales, solo, multijoueur, y compris le high score global) - * à leurs valeurs par défaut (0 ou équivalent). - * Après la réinitialisation, les nouvelles valeurs sont immédiatement sauvegardées - * dans les SharedPreferences via {@link #saveStats()}. + * Méthode privée pour mettre à jour les statistiques communes à la fin d'une partie multijoueur. + * Met à jour le nombre de parties jouées, le score total, le temps total et le meilleur score multi. + * + * @param score Le score obtenu dans la partie multijoueur terminée. + * @param durationMs La durée de la partie multijoueur (en millisecondes). + */ + private void endMultiplayerGame(int score, long durationMs) { + multiplayerGamesPlayed++; + multiplayerTotalScore += score; + if (durationMs > 0) { + multiplayerTotalTimeMs += durationMs; + } + if (score > multiplayerHighestScore) { + multiplayerHighestScore = score; + } + // Sauvegarde souvent après une partie multi pour ne pas perdre le résultat + saveStats(); + } + + /** + * Enregistre une victoire en multijoueur. + * Incrémente le compteur de victoires multi, met à jour la série de victoires (actuelle et meilleure), + * et appelle {@link #endMultiplayerGame(int, long)}. + * + * @param score Le score obtenu dans la partie gagnée. + * @param durationMs La durée de la partie gagnée (en millisecondes). + */ + public void recordMultiplayerWin(int score, long durationMs) { + multiplayerGamesWon++; + currentMultiplayerWinningStreak++; // Incrémente la série actuelle + if (currentMultiplayerWinningStreak > multiplayerBestWinningStreak) { + multiplayerBestWinningStreak = currentMultiplayerWinningStreak; // Met à jour la meilleure série si nécessaire + } + endMultiplayerGame(score, durationMs); // Finalise les stats communes + } + + /** + * Enregistre une défaite en multijoueur. + * Incrémente le compteur de défaites multi, réinitialise la série de victoires actuelle, + * et appelle {@link #endMultiplayerGame(int, long)}. + * + * @param score Le score obtenu dans la partie perdue. + * @param durationMs La durée de la partie perdue (en millisecondes). + */ + public void recordMultiplayerLoss(int score, long durationMs) { + totalMultiplayerLosses++; + currentMultiplayerWinningStreak = 0; // Réinitialise la série de victoires + endMultiplayerGame(score, durationMs); // Finalise les stats communes + } + + /** + * Enregistre un match nul en multijoueur. + * Réinitialise la série de victoires actuelle et appelle {@link #endMultiplayerGame(int, long)}. + * Note: Un match nul compte comme une partie jouée mais n'affecte pas les victoires/défaites. + * + * @param score Le score obtenu dans le match nul. + * @param durationMs La durée du match nul (en millisecondes). + */ + public void recordMultiplayerDraw(int score, long durationMs) { + currentMultiplayerWinningStreak = 0; // Réinitialise la série de victoires (convention) + endMultiplayerGame(score, durationMs); // Finalise les stats communes + } + + // --- Réinitialisation --- + + /** + * Réinitialise toutes les statistiques (solo et multijoueur, persistées et transitoires) + * à leurs valeurs par défaut. + * Sauvegarde immédiatement l'état réinitialisé dans les SharedPreferences. */ public void resetStats() { - // Réinitialise toutes les variables membres à 0 ou valeur initiale + // Réinitialisation Solo & Général overallHighScore = 0; totalGamesPlayed = 0; totalGamesStarted = 0; @@ -313,11 +360,9 @@ public class GameStats { totalMerges = 0; highestTile = 0; numberOfTimesObjectiveReached = 0; - // perfectGames = 0; // Supprimé bestWinningTimeMs = Long.MAX_VALUE; worstWinningTimeMs = 0L; - - // Réinitialise les stats multijoueur (placeholders) + // Réinitialisation Multi multiplayerGamesWon = 0; multiplayerGamesPlayed = 0; multiplayerBestWinningStreak = 0; @@ -325,96 +370,94 @@ public class GameStats { multiplayerTotalTimeMs = 0L; totalMultiplayerLosses = 0; multiplayerHighestScore = 0; - - // Réinitialise les stats de la partie en cours (au cas où) + // Réinitialisation Transitoire currentMoves = 0; mergesThisGame = 0; - currentGameStartTimeMs = 0L; // Ou System.currentTimeMillis() si on considère que reset démarre une nouvelle session? Non, 0 est plus sûr. + currentGameStartTimeMs = 0L; + currentMultiplayerWinningStreak = 0; - // Sauvegarde immédiatement les statistiques réinitialisées - saveStats(); + saveStats(); // Sauvegarde l'état réinitialisé } - // --- Getters pour l'affichage --- + // --- Getters (Accesseurs) --- - /** @return Le nombre total de parties terminées (victoire ou défaite). */ + /** @return Le nombre total de parties solo terminées. */ public int getTotalGamesPlayed() { return totalGamesPlayed; } - /** @return Le nombre total de parties démarrées. */ + /** @return Le nombre total de parties solo démarrées. */ public int getTotalGamesStarted() { return totalGamesStarted; } - /** @return Le nombre total de mouvements valides effectués. */ + /** @return Le nombre total de mouvements effectués en solo. */ public int getTotalMoves() { return totalMoves; } - /** @return Le nombre de mouvements dans la partie en cours. */ + /** @return Le nombre de mouvements dans la partie solo actuelle. */ public int getCurrentMoves() { return currentMoves; } - /** @return Le temps total de jeu accumulé (en ms). */ + /** @return Le temps de jeu total cumulé en solo (millisecondes). */ public long getTotalPlayTimeMs() { return totalPlayTimeMs; } - /** @return Le timestamp (en ms) du début de la partie en cours. Utile pour calculer la durée en temps réel. */ + /** @return Le timestamp de début de la partie solo actuelle (millisecondes). */ public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } - /** @return Le nombre de fusions dans la partie en cours. */ + /** @return Le nombre de fusions dans la partie solo actuelle. */ public int getMergesThisGame() { return mergesThisGame; } - /** @return Le nombre total de fusions effectuées. */ + /** @return Le nombre total de fusions effectuées en solo. */ public int getTotalMerges() { return totalMerges; } - /** @return La valeur de la plus haute tuile atteinte globalement. */ + /** @return La valeur de la plus haute tuile jamais atteinte en solo. */ public int getHighestTile() { return highestTile; } - /** @return Le nombre de fois où l'objectif (>= 2048) a été atteint. */ + /** @return Le nombre de fois où l'objectif a été atteint en solo. */ public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; } - // public int getPerfectGames() { return perfectGames; } // Supprimé - /** @return Le meilleur temps (le plus court, en ms) pour gagner une partie, ou Long.MAX_VALUE si aucune victoire. */ + /** @return Le meilleur temps pour atteindre l'objectif en solo (millisecondes), ou Long.MAX_VALUE si jamais atteint. */ public long getBestWinningTimeMs() { return bestWinningTimeMs; } - /** @return Le pire temps (le plus long, en ms) pour gagner une partie, ou 0 si aucune victoire. */ + /** @return Le pire temps pour atteindre l'objectif en solo (millisecondes). */ public long getWorstWinningTimeMs() { return worstWinningTimeMs; } - /** @return Le meilleur score global enregistré. */ + /** @return Le meilleur score global atteint (principalement solo). */ public int getOverallHighScore() { return overallHighScore; } - // Getters Multiplayer (fonctionnalité future) - /** @return Nombre de parties multijoueur gagnées. */ + // --- Getters Multijoueur --- + /** @return Le nombre total de parties multijoueur gagnées. */ public int getMultiplayerGamesWon() { return multiplayerGamesWon; } - /** @return Nombre de parties multijoueur jouées. */ + /** @return Le nombre total de parties multijoueur jouées. */ public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; } - /** @return Meilleure série de victoires consécutives en multijoueur. */ + /** @return La meilleure série de victoires consécutives en multijoueur. */ public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; } - /** @return Score total accumulé en multijoueur. */ + /** @return Le score total cumulé en multijoueur. */ public long getMultiplayerTotalScore() { return multiplayerTotalScore; } - /** @return Temps total passé en multijoueur (en ms). */ + /** @return Le temps de jeu total cumulé en multijoueur (millisecondes). */ public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; } - /** @return Nombre total de défaites en multijoueur. */ + /** @return Le nombre total de parties multijoueur perdues. */ public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; } - /** @return Meilleur score atteint dans une partie multijoueur. */ + /** @return Le meilleur score obtenu dans une seule partie multijoueur. */ public int getMultiplayerHighestScore() { return multiplayerHighestScore; } + /** @return La série de victoires consécutives actuelle en multijoueur (non persisté). */ + public int getCurrentMultiplayerWinningStreak() { return currentMultiplayerWinningStreak; } - // --- Setters --- + + // --- Setters (Mutateurs) --- /** - * Met à jour la valeur interne du meilleur score global (`overallHighScore`). - * La mise à jour n'a lieu que si le score fourni est strictement supérieur - * au meilleur score actuel. La sauvegarde effective dans SharedPreferences - * se fait via un appel ultérieur à {@link #saveStats()}. + * Met à jour le meilleur score global si le score fourni est supérieur. + * Utile pour enregistrer un nouveau record après une partie solo. + * Ne sauvegarde pas automatiquement, appeler {@link #saveStats()} si nécessaire. * - * @param highScore Le nouveau score à potentiellement définir comme meilleur score. + * @param score Le nouveau score potentiellement plus élevé. */ - public void setHighestScore(int highScore) { - // Met à jour seulement si la nouvelle valeur est meilleure - if (highScore > this.overallHighScore) { - this.overallHighScore = highScore; - // La sauvegarde se fait via saveStats() globalement, pas ici directement. + public void setHighestScore(int score) { + if (score > overallHighScore) { + overallHighScore = score; } } /** - * Définit le timestamp (en ms) du début de la partie en cours. - * Typiquement utilisé par MainActivity dans `onResume` pour reprendre le chronomètre. + * Définit manuellement l'heure de début de la partie en cours. + * Peut être utile si l'état du jeu est restauré d'une source externe. * - * @param timeMs Le timestamp en millisecondes. + * @param timeMs Le timestamp de début en millisecondes. */ public void setCurrentGameStartTimeMs(long timeMs) { - this.currentGameStartTimeMs = timeMs; + currentGameStartTimeMs = timeMs; } // --- Méthodes Calculées --- /** - * Calcule le temps moyen passé par partie terminée (solo). + * Calcule le temps de jeu moyen par partie solo terminée. * - * @return Le temps moyen par partie en millisecondes, ou 0 si aucune partie n'a été terminée. + * @return Le temps moyen en millisecondes, ou 0 si aucune partie n'a été jouée. */ public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0L; @@ -422,44 +465,44 @@ public class GameStats { /** * Calcule le score moyen par partie multijoueur terminée. - * (Basé sur les statistiques multijoueur placeholder). * - * @return Le score moyen entier par partie multijoueur, ou 0 si aucune partie multijoueur n'a été jouée. + * @return Le score moyen, ou 0 si aucune partie multijoueur n'a été jouée. */ public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; } /** - * Calcule le temps moyen passé par partie multijoueur terminée. - * (Basé sur les statistiques multijoueur placeholder). + * Calcule le temps de jeu moyen par partie multijoueur terminée. * - * @return Le temps moyen par partie multijoueur en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée. + * @return Le temps moyen en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée. */ public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0L; } /** - * Formate une durée donnée en millisecondes en une chaîne de caractères lisible. - * Le format est "hh:mm:ss" si la durée est d'une heure ou plus, sinon "mm:ss". + * Formate une durée en millisecondes en une chaîne lisible "HH:MM:SS" ou "MM:SS". + * Gère les durées négatives en les traitant comme 0. + * Supprime le lint warning pour DefaultLocale car le format numérique est indépendant de la locale. * - * @param milliseconds Durée totale en millisecondes. + * @param ms La durée en millisecondes à formater. * @return Une chaîne formatée représentant la durée. */ - @SuppressLint("DefaultLocale") // Justifié car le format hh:mm:ss est standard et non dépendant de la locale - public static String formatTime(long milliseconds) { - if (milliseconds < 0) { milliseconds = 0; } // Gère les cas négatifs - - long hours = TimeUnit.MILLISECONDS.toHours(milliseconds); - long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; // Minutes restantes après les heures - long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60; // Secondes restantes après les minutes + @SuppressLint("DefaultLocale") // Le format %02d est indépendant de la locale + public static String formatTime(long ms) { + if (ms < 0) { + ms = 0; // Traite les durées négatives comme 0 + } + long hours = TimeUnit.MILLISECONDS.toHours(ms); + long minutes = TimeUnit.MILLISECONDS.toMinutes(ms) % 60; // Minutes restantes après les heures + long seconds = TimeUnit.MILLISECONDS.toSeconds(ms) % 60; // Secondes restantes après les minutes if (hours > 0) { - // Format avec heures si nécessaire + // Format avec heures si la durée est >= 1 heure return String.format("%02d:%02d:%02d", hours, minutes, seconds); } else { - // Format minutes:secondes sinon + // Format sans heures si la durée est < 1 heure return String.format("%02d:%02d", minutes, seconds); } } diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index b493708..951a89a 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -1,11 +1,3 @@ -/** - * Activité principale de l'application Best 2048. - * Gère l'interface utilisateur (plateau de jeu, affichage des scores, boutons), - * coordonne les interactions de l'utilisateur (swipes, clics) avec la logique du jeu - * (classe {@link Game}) et la gestion des statistiques (classe {@link GameStats}). - * Prend également en charge le cycle de vie de l'application, la persistance de l'état du jeu - * via SharedPreferences, les effets sonores via SoundPool, et les notifications. - */ package legion.muyue.best2048; import static androidx.core.content.ContextCompat.startActivity; @@ -13,10 +5,6 @@ import static androidx.core.content.ContextCompat.startActivity; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; -import android.app.NotificationChannel; // Conservé pour référence mais createNotificationChannel est dans Helper -import android.app.NotificationManager; // Conservé pour référence mais createNotificationChannel est dans Helper -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; import android.content.pm.PackageManager; import android.os.Build; import android.provider.Settings; @@ -25,7 +13,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.util.Log; // Utiliser Log pour le débogage import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; @@ -39,8 +26,6 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; -import androidx.core.app.NotificationCompat; // Conservé pour référence, mais showNotification est dans Helper -import androidx.core.app.NotificationManagerCompat; // Conservé pour référence, mais showNotification est dans Helper import androidx.core.content.ContextCompat; import androidx.gridlayout.widget.GridLayout; import android.widget.Button; @@ -49,253 +34,228 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; -import android.view.ViewTreeObserver; +import android.content.ActivityNotFoundException; import android.media.AudioAttributes; import android.media.SoundPool; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; + import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +/** + * Activité principale de l'application, gérant le mode de jeu solo 2048. + * Responsable de l'affichage du plateau de jeu, des scores, de la gestion des entrées utilisateur (swipes, clics sur boutons), + * de l'intégration avec la logique du jeu (classe {@link Game}), de la gestion des statistiques (classe {@link GameStats}), + * des effets sonores ({@link SoundPool}), des paramètres (son, notifications), de la sauvegarde et du chargement de l'état du jeu + * via {@link SharedPreferences}, et de la planification des tâches de notification en arrière-plan avec {@link WorkManager}. + */ public class MainActivity extends AppCompatActivity { - // --- Constantes --- - /** Tag pour les logs de débogage spécifiques à cette activité. */ - private static final String TAG = "MainActivity"; - /** Taille du plateau de jeu (nombre de lignes/colonnes). Doit correspondre à Game.BOARD_SIZE. */ + /** Taille du plateau de jeu (4x4). */ private static final int BOARD_SIZE = 4; - /** Identifiant unique du canal de notification. Doit correspondre à NotificationHelper.CHANNEL_ID. */ - private static final String NOTIFICATION_CHANNEL_ID = "BEST_2048_CHANNEL"; - /** Nom du fichier de préférences partagées. Doit correspondre à GameStats.PREFS_NAME. */ + /** Nom du fichier SharedPreferences pour stocker les préférences et l'état du jeu. */ private static final String PREFS_NAME = "Best2048_Prefs"; - /** Clé pour sauvegarder/charger le meilleur score global. Doit correspondre à GameStats.HIGH_SCORE_KEY. */ + /** Clé SharedPreferences pour le meilleur score. */ private static final String HIGH_SCORE_KEY = "high_score"; - /** Clé pour sauvegarder/charger l'état sérialisé du jeu (plateau + score courant). */ + /** Clé SharedPreferences pour l'état sérialisé du jeu solo. */ private static final String GAME_STATE_KEY = "game_state"; - /** Clé pour sauvegarder/charger le timestamp de la dernière partie jouée (pour les notifications d'inactivité). */ + /** Clé SharedPreferences pour le timestamp de la dernière partie jouée. */ private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; - /** Clé pour sauvegarder/charger la préférence d'activation du son. */ + /** Clé SharedPreferences pour l'état d'activation des sons. */ private static final String SOUND_ENABLED_KEY = "sound_enabled"; - /** Clé pour sauvegarder/charger la préférence d'activation des notifications. */ + /** Clé SharedPreferences pour l'état d'activation des notifications. */ private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled"; + /** Tag unique pour identifier le travail périodique de notification dans WorkManager. */ + private static final String PERIODIC_NOTIFICATION_WORK_TAG = "periodic-notification-work"; + /** Intervalle de répétition (en heures) pour le worker de notification. */ + private static final long WORKER_REPEAT_INTERVAL_HOURS = 12; // --- UI Elements --- - /** Layout de grille affichant les tuiles du jeu. */ + /** Layout de grille pour le plateau de jeu. */ private GridLayout boardGridLayout; - /** TextView affichant le score de la partie en cours. */ + /** TextView affichant le score actuel. */ private TextView currentScoreTextView; - /** TextView affichant le meilleur score global. */ + /** TextView affichant le meilleur score. */ private TextView highestScoreTextView; - /** Bouton pour démarrer une nouvelle partie (après confirmation). */ + /** Bouton pour démarrer une nouvelle partie. */ private Button newGameButton; - /** Bouton pour accéder aux fonctionnalités multijoueur (placeholder actuel). */ + /** Bouton pour accéder au mode multijoueur. */ private Button multiplayerButton; - /** Bouton pour afficher/masquer le panneau des statistiques. */ + /** Bouton pour afficher/masquer les statistiques. */ private Button statisticsButton; - /** Bouton pour ouvrir le menu principal (dialogue). */ + /** Bouton pour ouvrir le menu principal. */ private Button menuButton; - /** ViewStub pour charger paresseusement le layout des statistiques. */ + /** ViewStub pour charger paresseusement le panneau des statistiques. */ private ViewStub statisticsViewStub; - /** Référence au layout des statistiques une fois qu'il est gonflé par le ViewStub. */ + /** Référence vers la vue des statistiques une fois chargée. */ private View inflatedStatsView; - /** Références aux TextViews représentant les tuiles actuellement affichées sur le plateau. */ + /** Tableau 2D de TextViews représentant les tuiles sur le plateau. */ private TextView[][] tileViews = new TextView[BOARD_SIZE][BOARD_SIZE]; - // --- Game Logic & Stats --- - /** Instance de la classe Game contenant la logique métier du jeu (plateau, mouvements, score). */ + // --- Game Logic & State --- + /** Instance de la logique du jeu 2048. */ private Game game; - /** Instance de la classe GameStats gérant la collecte et la persistance des statistiques. */ + /** Instance pour gérer les statistiques de jeu. */ private GameStats gameStats; - - // --- State Management --- - /** Indique si le panneau des statistiques est actuellement visible. */ + /** Indicateur si le panneau des statistiques est actuellement visible. */ private boolean statisticsVisible = false; - /** Énumération pour suivre l'état actuel du flux de jeu (en cours, gagné, perdu). */ + /** Énumération pour suivre l'état global du flux de jeu (en cours, gagné, perdu). */ private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } /** État actuel du flux de jeu. */ private GameFlowState currentGameState = GameFlowState.PLAYING; - /** Indique si les notifications sont activées par l'utilisateur et si la permission est accordée (sur API 33+). */ + /** Indicateur si les notifications sont activées dans les préférences. */ private boolean notificationsEnabled = false; - /** Indique si les effets sonores sont activés par l'utilisateur. */ - private boolean soundEnabled = true; // Son activé par défaut - - // --- Preferences --- - /** Instance de SharedPreferences pour la persistance des données. */ + /** Indicateur si les effets sonores sont activés dans les préférences. */ + private boolean soundEnabled = true; + /** Instance des SharedPreferences. */ private SharedPreferences preferences; // --- Sound --- - /** Pool pour gérer et jouer les effets sonores courts. */ + /** Pool pour gérer et jouer les effets sonores. */ private SoundPool soundPool; - /** ID du son chargé pour un mouvement de tuile. */ + /** ID du son pour un mouvement de tuile. */ private int soundMoveId = -1; - /** ID du son chargé pour une fusion de tuiles. */ + /** ID du son pour une fusion de tuiles. */ private int soundMergeId = -1; - /** ID du son chargé pour la victoire (atteinte de 2048). */ + /** ID du son pour la victoire (atteinte de 2048). */ private int soundWinId = -1; - /** ID du son chargé pour la fin de partie (Game Over). */ + /** ID du son pour la fin de partie (game over). */ private int soundGameOverId = -1; /** Indicateur si tous les sons ont été chargés avec succès dans le SoundPool. */ private boolean soundPoolLoaded = false; /** - * Lanceur d'activité pour demander la permission POST_NOTIFICATIONS (Android 13+). - * Gère la réponse de l'utilisateur (accordée ou refusée). + * Launcher pour gérer le résultat de la demande de permission de notification + * (requise à partir d'Android 13). */ private final ActivityResultLauncherIl est recommandé d'appeler {@link #createNotificationChannel(Context)} une fois + * au démarrage de l'application (par exemple, dans la méthode {@code onCreate} de la classe Application) + * pour s'assurer que le canal est disponible avant d'envoyer des notifications sur les appareils + * Android 8.0+.
+ */ public class NotificationHelper { /** - * Identifiant unique et constant pour le canal de notification de cette application. - * Ce même ID doit être utilisé lors de la création de la notification via {@link NotificationCompat.Builder}. - * Il est visible par l'utilisateur dans les paramètres de notification de l'application sur Android 8.0+. + * Identifiant unique et constant pour le canal de notification de l'application. + * Requis pour Android 8.0 (API niveau 26) et supérieur. */ - public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé dans MainActivity/Service - - /** Tag pour les logs spécifiques à ce helper. */ - private static final String TAG = "NotificationHelper"; + public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; /** - * Crée le canal de notification requis pour afficher des notifications sur Android 8.0 (API 26) et supérieur. - * Si le canal existe déjà, cette méthode ne fait rien (elle est idempotente). - * Doit être appelée une fois, idéalement au démarrage de l'application (par exemple dans `Application.onCreate` - * ou dans `MainActivity.onCreate`), avant d'afficher la toute première notification sur un appareil API 26+. + * Constructeur privé pour empêcher l'instanciation de cette classe utilitaire. + */ + private NotificationHelper() { + // Classe utilitaire, ne doit pas être instanciée. + } + + /** + * Crée le canal de notification nécessaire pour afficher des notifications sur + * Android 8.0 (Oreo, API niveau 26) et supérieur. + * Si l'application cible une version d'Android inférieure ou si le canal existe déjà, + * cette méthode n'a aucun effet ou n'est pas nécessaire. + * Doit être appelée avant de tenter d'afficher une notification sur les appareils concernés. + * Utilise des ressources string ({@code R.string}) pour le nom et la description du canal. * - * @param context Le contexte applicatif ({@code Context}) utilisé pour accéder aux services système. + * @param context Le contexte de l'application, nécessaire pour accéder aux services système + * et aux ressources. Ne doit pas être null. */ public static void createNotificationChannel(@NonNull Context context) { - // La création de canal n'est nécessaire que sur Android Oreo (API 26) et versions ultérieures if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Récupère les chaînes de caractères pour le nom et la description du canal depuis les ressources CharSequence name = context.getString(R.string.notification_channel_name); String description = context.getString(R.string.notification_channel_description); - // Définit l'importance du canal (affecte comment la notification est présentée à l'utilisateur) - // IMPORTANCE_DEFAULT est un bon point de départ pour la plupart des notifications. - int importance = NotificationManager.IMPORTANCE_DEFAULT; + // Définit l'importance du canal (affecte comment la notification est présentée) + int importance = NotificationManager.IMPORTANCE_DEFAULT; // Ou IMPORTANCE_HIGH, etc. // Crée l'objet NotificationChannel NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); channel.setDescription(description); - // Optionnel : Configurer d'autres propriétés du canal ici (vibration, lumière, son par défaut, etc.) - // channel.enableLights(true); - // channel.setLightColor(Color.RED); - // channel.enableVibration(true); - // channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); // Récupère le NotificationManager système NotificationManager notificationManager = context.getSystemService(NotificationManager.class); // Enregistre le canal auprès du système. Si le canal existe déjà, aucune opération n'est effectuée. if (notificationManager != null) { - Log.d(TAG, "Création ou mise à jour du canal de notification: " + CHANNEL_ID); notificationManager.createNotificationChannel(channel); } else { - Log.e(TAG, "NotificationManager non disponible, impossible de créer le canal."); } - } else { - Log.d(TAG, "API < 26, pas besoin de créer de canal de notification."); } } /** * Construit et affiche une notification standard pour l'application. - * Vérifie la permission {@code POST_NOTIFICATIONS} sur Android 13 (API 33) et supérieur - * avant de tenter d'afficher la notification. Si la permission est manquante, - * un message d'erreur est logué et la méthode se termine sans afficher de notification. + * Gère la vérification de la permission {@code POST_NOTIFICATIONS} pour Android 13+ (Tiramisu). + * Configure un {@link PendingIntent} pour ouvrir {@link MainActivity} lorsque l'utilisateur + * appuie sur la notification. + * Utilise {@link NotificationCompat} pour la compatibilité avec les anciennes versions d'Android. * - * @param context Le contexte ({@code Context}, peut être une Activity, un Service, etc.) - * utilisé pour construire la notification et accéder aux ressources. - * @param title Le titre à afficher dans la notification. - * @param message Le contenu textuel principal de la notification. - * @param notificationId Un identifiant entier unique pour cette notification. Permet de mettre à jour - * ou d'annuler cette notification spécifique plus tard en utilisant le même ID. + * @param context Le contexte de l'application. Ne doit pas être null. + * @param title Le titre de la notification. Ne doit pas être null. + * @param message Le message principal (corps) de la notification. Ne doit pas être null. + * @param notificationId Un identifiant entier unique pour cette notification. Si une notification + * avec le même ID existe déjà, elle sera mise à jour. */ public static void showNotification(@NonNull Context context, @NonNull String title, @NonNull String message, int notificationId) { - Log.d(TAG, "Tentative d'affichage de la notification ID: " + notificationId + " Titre: " + title); - - // --- Vérification de la permission pour Android 13 (API 33) et supérieur --- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // Utilise ActivityCompat.checkSelfPermission pour vérifier la permission + // Vérifie si la permission d'envoyer des notifications est accordée if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - // Si la permission n'est pas accordée, loguer un avertissement et ne pas continuer. - // L'application appelante (ex: MainActivity, Service) est responsable de demander la permission - // si l'utilisateur a indiqué vouloir recevoir des notifications. - Log.w(TAG, "Permission POST_NOTIFICATIONS manquante pour API 33+. Notification ID " + notificationId + " non affichée."); - // Optionnel : Envoyer un broadcast ou utiliser une autre méthode pour signaler l'échec ? Non, simple log suffit ici. - return; - } else { - Log.d(TAG, "Permission POST_NOTIFICATIONS accordée sur API 33+."); + // Si la permission n'est pas accordée, on ne peut pas afficher la notification. + // L'application devrait demander la permission à l'utilisateur à un moment approprié. + return; // Arrête l'exécution de la méthode } - } else { - Log.d(TAG, "API < 33, permission POST_NOTIFICATIONS non requise pour afficher."); } - // --- Création de l'Intent pour le clic sur la notification --- - // Ouvre MainActivity lorsque l'utilisateur clique sur la notification. + // Crée un Intent pour ouvrir MainActivity lorsque la notification est cliquée Intent intent = new Intent(context, MainActivity.class); - // Flags pour gérer correctement la pile d'activités (ramène l'instance existante ou en crée une nouvelle) + // Flags pour gérer le comportement de la pile d'activités intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - // Flags pour le PendingIntent (requis : IMMUTABLE sur API 31+) + + // Crée un PendingIntent qui enveloppe l'Intent int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE; } - // Crée le PendingIntent qui enveloppe l'Intent PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, pendingIntentFlags); - // --- Construction de la notification via NotificationCompat --- + // Construit la notification en utilisant NotificationCompat.Builder pour la compatibilité NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_stat_notification_2048) // Icône obligatoire (doit être monochrome/blanche avec transparence) - // .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher)) // Icône optionnelle plus grande - .setContentTitle(title) // Titre de la notification - .setContentText(message) // Texte principal - .setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité (affecte l'affichage head-up, etc.) - .setContentIntent(pendingIntent) // Action à exécuter au clic - .setAutoCancel(true) // Ferme automatiquement la notification après le clic - .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) // Utilise son/vibration par défaut du système (si autorisé) + .setSmallIcon(R.drawable.ic_stat_notification_2048) // Icône obligatoire + .setContentTitle(title) // Titre de la notification + .setContentText(message) // Corps du message + .setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité (affecte l'affichage) + .setContentIntent(pendingIntent) // Action lorsque l'utilisateur clique sur la notification + .setAutoCancel(true) // Ferme la notification après le clic + .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) // Son et vibration par défaut .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); // Visible sur l'écran de verrouillage - // --- Affichage de la notification --- - // Utilise NotificationManagerCompat pour la compatibilité + // Récupère NotificationManagerCompat pour envoyer la notification NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); try { - // Affiche la notification. Si une notification avec le même ID existe déjà, elle est mise à jour. notificationManager.notify(notificationId, builder.build()); - Log.i(TAG, "Notification ID " + notificationId + " affichée avec succès."); } catch (SecurityException e){ - // Gérer l'exception de sécurité qui peut (rarement) survenir même avec la vérification ci-dessus. - Log.e(TAG, "Erreur de sécurité lors de l'affichage de la notification ID " + notificationId, e); - // Afficher un Toast ou logguer est généralement suffisant ici. } catch (Exception ex) { - // Capturer d'autres exceptions potentielles - Log.e(TAG, "Erreur inattendue lors de l'affichage de la notification ID " + notificationId, ex); } } } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/NotificationService.java b/app/src/main/java/legion/muyue/best2048/NotificationService.java deleted file mode 100644 index 26bdba2..0000000 --- a/app/src/main/java/legion/muyue/best2048/NotificationService.java +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Service exécuté en arrière-plan pour tenter d'envoyer des notifications périodiques, - * comme un rappel du meilleur score ou un rappel d'inactivité. - * - *AVERTISSEMENT IMPORTANT : Ce service utilise un {@link android.os.Handler} - * pour planifier les tâches répétitives. Cette approche est simple mais NON FIABLE - * pour les tâches en arrière-plan sur les versions modernes d'Android. Le système peut tuer - * le service à tout moment pour économiser les ressources, et le redémarrage via - * {@code START_STICKY} n'est pas garanti d'être rapide ou même de se produire sur tous les appareils - * ou dans toutes les conditions (ex: mode Doze, restrictions fabricant).
- * - *Pour une solution robuste et recommandée par Android pour les tâches périodiques - * en arrière-plan, il est FORTEMENT conseillé d'utiliser {@link androidx.work.WorkManager}. - * WorkManager gère les contraintes (réseau, charge), la persistance des tâches et - * garantit leur exécution même si l'application ou l'appareil redémarre.
- * - *Ce code est fourni à titre d'exemple de la logique de notification, mais le mécanisme - * de planification devrait être remplacé par WorkManager en production.
- */ -package legion.muyue.best2048; - -import android.app.Service; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; // Important pour créer un Handler sur le Main Thread -import android.util.Log; - -import androidx.annotation.Nullable; -import java.util.concurrent.TimeUnit; - -public class NotificationService extends Service { - - /** Tag pour les logs spécifiques à ce service. */ - private static final String TAG = "NotificationService"; - - // --- Constantes de Notification et Intervalles --- - /** ID unique pour la notification de rappel du meilleur score. */ - private static final int NOTIFICATION_ID_HIGHSCORE = 2; // Doit être différent des autres notifs (ex: 1 pour achievement) - /** ID unique pour la notification de rappel d'inactivité. */ - private static final int NOTIFICATION_ID_INACTIVITY = 3; - - /** Intervalle minimum souhaité entre deux notifications de rappel du meilleur score (ex: 1 jour). */ - private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); - /** Seuil d'inactivité : durée après laquelle une notification d'inactivité peut être envoyée (ex: 3 jours). */ - private static final long INACTIVITY_THRESHOLD_MS = TimeUnit.DAYS.toMillis(3); - /** Intervalle minimum souhaité entre deux notifications de rappel d'inactivité (ex: 1 jour, pour éviter spam si toujours inactif). */ - private static final long INACTIVITY_NOTIFICATION_COOLDOWN_MS = TimeUnit.DAYS.toMillis(1); - - /** Intervalle auquel le Handler vérifie s'il faut envoyer une notification (ex: toutes les 6 heures). */ - // NOTE: Avec WorkManager, cette vérification fréquente serait inutile, on planifierait directement la tâche au bon moment. - private static final long CHECK_INTERVAL_MS = TimeUnit.HOURS.toMillis(6); - - /** Handler pour poster les tâches Runnable périodiques. Fonctionne sur le thread principal. */ - private Handler handler; - /** Runnable contenant la logique de vérification et d'envoi des notifications. */ - private Runnable periodicTaskRunnable; - - // --- Constantes SharedPreferences --- - // (Doivent correspondre à celles utilisées dans MainActivity et GameStats) - /** Nom du fichier de préférences partagées. */ - private static final String PREFS_NAME = "Best2048_Prefs"; - /** Clé pour lire le meilleur score global. */ - private static final String HIGH_SCORE_KEY = "high_score"; - /** Clé pour lire le timestamp de la dernière partie jouée. */ - private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; - /** Clé pour vérifier si les notifications sont activées par l'utilisateur. */ - private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled"; - /** Clé pour stocker le timestamp du dernier envoi de la notification High Score. */ - private static final String LAST_HS_NOTIFICATION_TIME = "lastHsNotificationTime"; - /** Clé pour stocker le timestamp du dernier envoi de la notification d'Inactivité. */ - private static final String LAST_INACTIVITY_NOTIFICATION_TIME = "lastInactivityNotificationTime"; - - /** - * Appelée par le système lors de la première création du service. - * Initialise le Handler et le Runnable pour la tâche périodique. - * Ne crée PAS le canal de notification ici, car MainActivity le fait déjà. - */ - @Override - public void onCreate() { - super.onCreate(); - Log.i(TAG, "onCreate: Service de notification en cours de création."); - // Utilise le Looper principal pour le Handler. Simple, mais bloque le thread principal - // si checkAndSendNotifications() devient une tâche longue. - // Pour des tâches potentiellement longues, utiliser un HandlerThread serait mieux. - handler = new Handler(Looper.getMainLooper()); - - // Définit le Runnable qui sera exécuté périodiquement - periodicTaskRunnable = new Runnable() { - @Override - public void run() { - Log.d(TAG, "Exécution de la tâche périodique de vérification des notifications."); - // Vérifie s'il faut envoyer des notifications - checkAndSendNotifications(); - // Replanifie la prochaine exécution de cette même tâche - // ATTENTION: Ce mécanisme n'est pas fiable en arrière-plan. - handler.postDelayed(this, CHECK_INTERVAL_MS); - Log.d(TAG, "Prochaine vérification des notifications planifiée dans " + CHECK_INTERVAL_MS + " ms."); - } - }; - } - - /** - * Appelée lorsque le service est démarré, soit par `startService()` soit après un redémarrage du système - * (si START_STICKY est utilisé et que le système choisit de le redémarrer). - * Lance la tâche périodique de vérification des notifications. - * - * @param intent L'Intent fourni à startService(), peut être null si redémarré par le système. - * @param flags Indicateurs supplémentaires sur la demande de démarrage. - * @param startId Un identifiant unique représentant cette demande de démarrage spécifique. - * @return La valeur de retour indique comment le système doit gérer le service s'il est tué. - * {@code START_STICKY} : Le système essaiera de recréer le service après l'avoir tué, - * mais l'Intent ne sera pas redélivré (il sera null). - * NOTE: START_STICKY n'est pas une garantie d'exécution fiable pour les tâches - * périodiques en arrière-plan. Utilisez WorkManager. - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.i(TAG, "onStartCommand: Service démarré (ou redémarré). StartId: " + startId); - - // S'assure qu'il n'y a pas d'anciennes tâches en double avant de poster la nouvelle - handler.removeCallbacks(periodicTaskRunnable); - // Lance la première exécution de la tâche immédiatement (ou presque) - handler.post(periodicTaskRunnable); - Log.d(TAG, "Première vérification des notifications postée."); - - // Utiliser START_STICKY pour que le système essaie de redémarrer le service s'il est tué. - // C'est un pis-aller par rapport à WorkManager. - return START_STICKY; - } - - /** - * Appelée lorsque le service est sur le point d'être détruit. - * Nettoie les ressources, en particulier, arrête la planification des tâches - * périodiques via le Handler pour éviter les fuites ou les exécutions non désirées. - */ - @Override - public void onDestroy() { - super.onDestroy(); - Log.i(TAG, "onDestroy: Service de notification en cours de destruction."); - // Arrête la planification des tâches lorsque le service est détruit - if (handler != null && periodicTaskRunnable != null) { - Log.d(TAG, "Annulation des tâches périodiques planifiées."); - handler.removeCallbacks(periodicTaskRunnable); - } - // Autres nettoyages si nécessaire - } - - /** - * Retourne l'interface de communication au client. - * Comme c'est un service démarré (Started Service) et non lié (Bound Service), - * cette méthode retourne {@code null}. - * - * @param intent L'Intent qui a été utilisé pour se lier au service (non pertinent ici). - * @return null car ce service ne permet pas le binding. - */ - @Nullable - @Override - public IBinder onBind(Intent intent) { - // Ce service n'est pas conçu pour être lié. - return null; - } - - /** - * Vérifie les conditions pour envoyer les notifications périodiques (High Score, Inactivité). - * Lit les préférences utilisateur (notifications activées?) et les timestamps nécessaires. - * Appelle les méthodes d'affichage de notification appropriées si les conditions et - * les délais depuis le dernier envoi sont respectés. - * Met à jour le timestamp du dernier envoi après avoir tenté d'envoyer une notification. - */ - private void checkAndSendNotifications() { - Log.d(TAG, "checkAndSendNotifications: Début de la vérification."); - SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - - // 1. Vérifier si les notifications sont globalement activées par l'utilisateur - boolean notificationsEnabled = prefs.getBoolean(NOTIFICATIONS_ENABLED_KEY, false); // Lire la préférence - if (!notificationsEnabled) { - Log.i(TAG, "checkAndSendNotifications: Notifications désactivées dans les préférences, arrêt de la vérification."); - // Si les notifications sont désactivées, on pourrait arrêter le service pour économiser des ressources. - // stopSelf(); // Arrêterait le service lui-même. - return; - } - - long currentTime = System.currentTimeMillis(); - - // 2. Vérification pour la notification High Score - long lastHsNotificationTime = prefs.getLong(LAST_HS_NOTIFICATION_TIME, 0); - if (currentTime - lastHsNotificationTime > HIGHSCORE_INTERVAL_MS) { - Log.d(TAG, "checkAndSendNotifications: Intervalle pour notification High Score écoulé."); - int highScore = prefs.getInt(HIGH_SCORE_KEY, 0); - if (highScore > 0) { // N'envoie pas si le high score est 0 - Log.i(TAG, "checkAndSendNotifications: Envoi de la notification High Score."); - showHighScoreNotificationNow(highScore); - // Met à jour le timestamp du dernier envoi APRÈS avoir tenté d'envoyer - prefs.edit().putLong(LAST_HS_NOTIFICATION_TIME, currentTime).apply(); - } else { - Log.d(TAG, "checkAndSendNotifications: High Score est 0, notification non envoyée."); - } - } else { - Log.d(TAG, "checkAndSendNotifications: Intervalle pour notification High Score non écoulé."); - } - - // 3. Vérification pour la notification d'Inactivité - long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0); - long lastInactivityNotificationTime = prefs.getLong(LAST_INACTIVITY_NOTIFICATION_TIME, 0); - - // Condition 1: Temps depuis la dernière partie > Seuil d'inactivité - boolean isInactivityThresholdMet = (lastPlayedTime > 0 && currentTime - lastPlayedTime > INACTIVITY_THRESHOLD_MS); - // Condition 2: Temps depuis la dernière notification d'inactivité > Cooldown - boolean isCooldownMet = (currentTime - lastInactivityNotificationTime > INACTIVITY_NOTIFICATION_COOLDOWN_MS); - - if (isInactivityThresholdMet) { - Log.d(TAG, "checkAndSendNotifications: Seuil d'inactivité atteint."); - if (isCooldownMet) { - Log.i(TAG, "checkAndSendNotifications: Cooldown pour notification d'inactivité respecté. Envoi..."); - showInactivityNotificationNow(); - // Met à jour le timestamp du dernier envoi APRÈS avoir tenté d'envoyer - prefs.edit().putLong(LAST_INACTIVITY_NOTIFICATION_TIME, currentTime).apply(); - } else { - Log.d(TAG, "checkAndSendNotifications: Cooldown pour notification d'inactivité non écoulé."); - } - } else { - Log.d(TAG, "checkAndSendNotifications: Seuil d'inactivité non atteint."); - } - Log.d(TAG, "checkAndSendNotifications: Fin de la vérification."); - } - - /** - * Prépare et affiche immédiatement la notification de rappel du meilleur score. - * Utilise {@link NotificationHelper} pour l'affichage effectif. - * - * @param highScore Le meilleur score à afficher dans la notification. - */ - private void showHighScoreNotificationNow(int highScore) { - String title = getString(R.string.notification_title_highscore); - String message = getString(R.string.notification_text_highscore, highScore); - NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_HIGHSCORE); - } - - /** - * Prépare et affiche immédiatement la notification de rappel d'inactivité. - * Utilise {@link NotificationHelper} pour l'affichage effectif. - */ - private void showInactivityNotificationNow() { - String title = getString(R.string.notification_title_inactivity); - String message = getString(R.string.notification_text_inactivity); - NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_INACTIVITY); - } - -} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/NotificationWorker.java b/app/src/main/java/legion/muyue/best2048/NotificationWorker.java new file mode 100644 index 0000000..c5d4ceb --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/NotificationWorker.java @@ -0,0 +1,148 @@ +package legion.muyue.best2048; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.util.concurrent.TimeUnit; + +/** + * Un {@link Worker} Android conçu pour s'exécuter périodiquement en arrière-plan + * via {@link androidx.work.WorkManager}. + * Sa tâche est de vérifier si certaines conditions sont remplies pour envoyer + * des notifications à l'utilisateur, telles qu'un rappel de son meilleur score + * ou une notification d'inactivité pour l'encourager à rejouer. + * Les conditions, seuils et états (comme la dernière fois jouée ou notifiée) + * sont gérés via {@link SharedPreferences}. + */ +public class NotificationWorker extends Worker { + + // --- Constantes --- + + /** Identifiant unique pour la notification de rappel du meilleur score. */ + private static final int NOTIFICATION_ID_HIGHSCORE = 2; + /** Identifiant unique pour la notification d'inactivité. */ + private static final int NOTIFICATION_ID_INACTIVITY = 3; + + /** Intervalle minimum (en millisecondes) entre deux notifications de rappel du meilleur score. (1 jour) */ + private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); + /** Seuil d'inactivité (en millisecondes) : durée depuis la dernière partie jouée au-delà de laquelle une notification peut être envoyée. (3 jours) */ + private static final long INACTIVITY_THRESHOLD_MS = TimeUnit.DAYS.toMillis(3); + /** Temps de recharge minimum (en millisecondes) entre deux notifications d'inactivité. (1 jour) */ + private static final long INACTIVITY_NOTIFICATION_COOLDOWN_MS = TimeUnit.DAYS.toMillis(1); + + /** Nom du fichier SharedPreferences utilisé par cette classe et potentiellement d'autres (ex: GameStats). */ + private static final String PREFS_NAME = "Best2048_Prefs"; + /** Clé SharedPreferences pour le meilleur score (utilisé aussi par GameStats). */ + private static final String HIGH_SCORE_KEY = "high_score"; + /** Clé SharedPreferences pour stocker le timestamp de la dernière fois où l'utilisateur a joué. */ + private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; + /** Clé SharedPreferences pour l'activation globale des notifications par l'utilisateur. */ + private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled"; + /** Clé SharedPreferences pour stocker le timestamp de la dernière notification de meilleur score envoyée. */ + private static final String LAST_HS_NOTIFICATION_TIME = "lastHsNotificationTime"; + /** Clé SharedPreferences pour stocker le timestamp de la dernière notification d'inactivité envoyée. */ + private static final String LAST_INACTIVITY_NOTIFICATION_TIME = "lastInactivityNotificationTime"; + + /** + * Constructeur standard pour un {@link Worker}. + * + * @param context Le contexte de l'application. + * @param workerParams Paramètres pour le Worker, fournis par WorkManager. + */ + public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + /** + * Exécute la tâche principale du Worker en arrière-plan. + * Vérifie si les notifications sont activées, puis évalue les conditions pour + * envoyer une notification de meilleur score ou une notification d'inactivité. + * Met à jour les timestamps des dernières notifications envoyées si nécessaire. + * Cette méthode est appelée par WorkManager sur un thread d'arrière-plan. + * + * @return {@link Result#success()} si le travail s'est terminé (qu'une notification ait été envoyée ou non). + * Retourne {@link Result#failure()} ou {@link Result#retry()} en cas d'erreur non récupérable ou si le travail doit être retenté. + * Ici, on retourne toujours success() même en cas d'exception interne pour ne pas bloquer les exécutions futures, + * mais un logging serait approprié. + */ + @NonNull + @Override + public Result doWork() { + Context context = getApplicationContext(); + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + + // 1. Vérifier si les notifications sont activées globalement + boolean notificationsEnabled = prefs.getBoolean(NOTIFICATIONS_ENABLED_KEY, false); + if (!notificationsEnabled) { + return Result.success(); // Travail terminé avec succès (rien à faire) + } + + long currentTime = System.currentTimeMillis(); + + // 2. Vérification pour la notification du meilleur score + try { + long lastHsNotificationTime = prefs.getLong(LAST_HS_NOTIFICATION_TIME, 0); + // Vérifier si l'intervalle depuis la dernière notification de high score est dépassé + if (currentTime - lastHsNotificationTime > HIGHSCORE_INTERVAL_MS) { + int highScore = prefs.getInt(HIGH_SCORE_KEY, 0); + // Vérifier s'il y a un high score à afficher (> 0) + if (highScore > 0) { + showHighScoreNotificationNow(context, highScore); + // Mettre à jour le timestamp de la dernière notification de high score + prefs.edit().putLong(LAST_HS_NOTIFICATION_TIME, currentTime).apply(); + } + } + } catch (Exception e) { + } + + // 3. Vérification pour la notification d'inactivité + try { + long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0); + long lastInactivityNotificationTime = prefs.getLong(LAST_INACTIVITY_NOTIFICATION_TIME, 0); + + // Condition 1: L'utilisateur n'a pas joué depuis un certain temps (INACTIVITY_THRESHOLD_MS) + boolean isInactivityThresholdMet = (lastPlayedTime > 0 && currentTime - lastPlayedTime > INACTIVITY_THRESHOLD_MS); + // Condition 2: Assez de temps s'est écoulé depuis la dernière notification d'inactivité (cooldown) + boolean isCooldownMet = (currentTime - lastInactivityNotificationTime > INACTIVITY_NOTIFICATION_COOLDOWN_MS); + + if (isInactivityThresholdMet && isCooldownMet) { + showInactivityNotificationNow(context); + // Mettre à jour le timestamp de la dernière notification d'inactivité + prefs.edit().putLong(LAST_INACTIVITY_NOTIFICATION_TIME, currentTime).apply(); + } + } catch (Exception e) { + } + return Result.success(); + } + + /** + * Construit et affiche immédiatement la notification de rappel du meilleur score. + * Utilise une classe helper {@code NotificationHelper} (non fournie ici) pour la logique d'affichage réelle. + * Récupère les textes depuis les ressources de chaînes Android (R.string). + * + * @param context Le contexte applicatif. + * @param highScore Le meilleur score actuel de l'utilisateur à afficher. + */ + private void showHighScoreNotificationNow(Context context, int highScore) { + String title = context.getString(R.string.notification_title_highscore); + String message = context.getString(R.string.notification_text_highscore, highScore); + NotificationHelper.showNotification(context, title, message, NOTIFICATION_ID_HIGHSCORE); + } + + /** + * Construit et affiche immédiatement la notification d'inactivité. + * Utilise une classe helper {@code NotificationHelper} (non fournie ici) pour la logique d'affichage réelle. + * Récupère les textes depuis les ressources de chaînes Android (R.string). + * + * @param context Le contexte applicatif. + */ + private void showInactivityNotificationNow(Context context) { + String title = context.getString(R.string.notification_title_inactivity); + String message = context.getString(R.string.notification_text_inactivity); + NotificationHelper.showNotification(context, title, message, NOTIFICATION_ID_INACTIVITY); + } +} \ 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 a530780..e57e7a4 100644 --- a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java +++ b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java @@ -1,9 +1,3 @@ -// Fichier OnSwipeTouchListener.java -/** - * Listener de vue personnalisé qui détecte les gestes de balayage (swipe) - * dans les quatre directions cardinales et notifie un listener externe. - * Utilise {@link GestureDetector} pour l'analyse des gestes. - */ package legion.muyue.best2048; import android.annotation.SuppressLint; @@ -12,66 +6,85 @@ import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +/** + * Un {@link View.OnTouchListener} qui détecte les gestes de balayage (swipe) + * dans quatre directions (haut, bas, gauche, droite) sur une {@link View} associée. + * Utilise un {@link GestureDetector} pour interpréter les événements tactiles. + * Notifie un {@link SwipeListener} lorsqu'un balayage valide est détecté. + * + * Pour l'utiliser, créez une instance de cette classe en fournissant un {@link Context} + * et une implémentation de {@link SwipeListener}, puis attachez-la à la View souhaitée + * via {@link View#setOnTouchListener(View.OnTouchListener)}. + */ public class OnSwipeTouchListener implements View.OnTouchListener { - /** Détecteur de gestes standard d'Android. */ + /** Détecteur de gestes Android utilisé pour interpréter les événements tactiles. */ private final GestureDetector gestureDetector; - /** Listener externe à notifier lors de la détection d'un swipe. */ + /** L'écouteur qui sera notifié des événements de balayage détectés. */ private final SwipeListener listener; /** - * Interface à implémenter par les classes souhaitant réagir aux événements de swipe. + * Interface de callback à implémenter par les classes qui souhaitent être notifiées + * des événements de balayage détectés par {@link OnSwipeTouchListener}. */ public interface SwipeListener { - /** Appelée lorsqu'un swipe vers le haut est détecté. */ + /** Appelé lorsqu'un balayage vers le haut est détecté. */ void onSwipeTop(); - /** Appelée lorsqu'un swipe vers le bas est détecté. */ + /** Appelé lorsqu'un balayage vers le bas est détecté. */ void onSwipeBottom(); - /** Appelée lorsqu'un swipe vers la gauche est détecté. */ + /** Appelé lorsqu'un balayage vers la gauche est détecté. */ void onSwipeLeft(); - /** Appelée lorsqu'un swipe vers la droite est détecté. */ + /** Appelé lorsqu'un balayage vers la droite est détecté. */ void onSwipeRight(); } /** - * Constructeur. - * @param context Contexte applicatif, nécessaire pour `GestureDetector`. - * @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null. + * Construit une nouvelle instance de l'écouteur de balayage. + * + * @param context Le contexte de l'application ou de l'activité, nécessaire pour initialiser le {@link GestureDetector}. + * @param listener L'instance de {@link SwipeListener} qui recevra les notifications de balayage. Ne doit pas être null. */ public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) { this.gestureDetector = new GestureDetector(context, new GestureListener()); + // Stocke la référence vers l'écouteur fourni this.listener = listener; } /** - * Intercepte les événements tactiles sur la vue associée et les délègue - * au {@link GestureDetector} pour analyse. - * @param v La vue touchée. - * @param event L'événement tactile. - * @return true si le geste a été consommé par le détecteur, false sinon. + * Méthode appelée lorsque la {@link View} associée reçoit un événement tactile. + * Délègue le traitement de l'événement au {@link GestureDetector}. + * + * @param v La {@link View} qui a reçu l'événement tactile. + * @param event L'objet {@link MotionEvent} décrivant l'événement tactile. + * @return {@code true} si l'événement a été consommé par le {@link GestureDetector}, {@code false} sinon. */ @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { - // Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling), - // il retournera true, et l'événement ne sera pas propagé davantage. + // Passe l'événement tactile au GestureDetector pour analyse return gestureDetector.onTouchEvent(event); } /** - * Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide). + * Classe interne qui étend {@link GestureDetector.SimpleOnGestureListener} pour + * implémenter la logique de détection de balayage (spécifiquement dans {@code onFling}). */ private final class GestureListener extends GestureDetector.SimpleOnGestureListener { - /** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */ + /** Distance minimale (en pixels) qu'un doigt doit parcourir pour qu'un mouvement soit considéré comme un balayage. */ private static final int SWIPE_THRESHOLD = 100; - /** Vitesse minimale (en pixels/sec) pour qu'un mouvement soit considéré comme un swipe. */ + /** Vitesse minimale (en pixels par seconde) requise pour qu'un mouvement soit considéré comme un balayage (fling). */ private static final int SWIPE_VELOCITY_THRESHOLD = 100; /** - * Toujours retourner true pour onDown garantit que les événements suivants - * (comme onFling) seront bien reçus par ce listener. + * Appelée lorsque l'événement {@link MotionEvent#ACTION_DOWN} se produit. + * Doit retourner {@code true} pour indiquer que ce listener est intéressé + * par la séquence complète des événements tactiles (move, up, fling). + * + * @param e L'événement MotionEvent initial (ACTION_DOWN). + * @return Toujours {@code true}. */ @Override public boolean onDown(@NonNull MotionEvent e) { @@ -79,47 +92,61 @@ public class OnSwipeTouchListener implements View.OnTouchListener { } /** - * Appelée quand un geste de 'fling' (balayage rapide) est détecté. - * Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide - * et notifie le {@link SwipeListener} externe. + * Appelée lorsque le {@link GestureDetector} détecte un mouvement de "fling" + * (un glissement rapide suivi d'un relâchement). C'est ici que la logique + * de détection de balayage est implémentée. + * + * @param e1 L'événement {@link MotionEvent} initial (ACTION_DOWN) où le fling a commencé. Peut être null dans certains cas rares. + * @param e2 L'événement {@link MotionEvent} final (ACTION_UP) où le fling s'est terminé. + * @param velocityX La vélocité du fling sur l'axe X (pixels par seconde). + * @param velocityY La vélocité du fling sur l'axe Y (pixels par seconde). + * @return {@code true} si un balayage valide a été détecté et géré (c'est-à-dire qu'une méthode du listener a été appelée), {@code false} sinon. */ @Override - public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { - if (e1 == null) return false; // Point de départ est nécessaire + public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { + // Vérifie si l'événement initial est null (précaution) + if (e1 == null) { + return false; + } - boolean result = false; + boolean result = false; // Indique si un swipe a été détecté et traité try { + // Calcule la différence de position entre le début et la fin du mouvement float diffY = e2.getY() - e1.getY(); float diffX = e2.getX() - e1.getX(); - // Priorité au mouvement le plus ample (horizontal ou vertical) + // Détermine si le mouvement est principalement horizontal ou vertical if (Math.abs(diffX) > Math.abs(diffY)) { // Mouvement principalement horizontal + // Vérifie si la distance et la vitesse dépassent les seuils if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { if (diffX > 0) { + // Balayage vers la droite listener.onSwipeRight(); } else { + // Balayage vers la gauche listener.onSwipeLeft(); } - result = true; // Geste horizontal traité + result = true; // Un balayage horizontal a été traité } } else { // Mouvement principalement vertical + // Vérifie si la distance et la vitesse dépassent les seuils if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { if (diffY > 0) { + // Balayage vers le bas listener.onSwipeBottom(); } else { + // Balayage vers le haut listener.onSwipeTop(); } - result = true; // Geste vertical traité + result = true; // Un balayage vertical a été traité } } } catch (Exception exception) { - // En cas d'erreur inattendue, on logue discrètement. - System.err.println("Erreur dans OnSwipeTouchListener.onFling: " + exception.getMessage()); - // Ne pas crasher l'application pour une erreur de détection de geste. } + // Retourne true si un swipe a été détecté et traité, false sinon return result; } } -} // Fin OnSwipeTouchListener \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/GameInfo.java b/app/src/main/java/legion/muyue/best2048/data/GameInfo.java index 4d8b4a0..8459ddc 100644 --- a/app/src/main/java/legion/muyue/best2048/data/GameInfo.java +++ b/app/src/main/java/legion/muyue/best2048/data/GameInfo.java @@ -1,20 +1,86 @@ -package legion.muyue.best2048.data; // Créez un sous-package data si vous voulez +package legion.muyue.best2048.data; import com.google.gson.annotations.SerializedName; +/** + * Représente les informations de base d'une partie du jeu 2048. + * Cette classe est utilisée pour stocker et transférer les données relatives + * à une session de jeu spécifique, notamment via la sérialisation/désérialisation JSON avec Gson. + */ public class GameInfo { - @SerializedName("gameId") // Correspond au nom du champ JSON + + /** + * L'identifiant unique de la partie. + * Utilisé par Gson pour la sérialisation avec le nom "gameId". + */ + @SerializedName("gameId") private String gameId; - @SerializedName("status") // Ex: WAITING, PLAYING, FINISHED + + /** + * Le statut actuel de la partie (par exemple, "en attente", "en cours", "terminée"). + * Utilisé par Gson pour la sérialisation avec le nom "status". + */ + @SerializedName("status") private String status; + + /** + * L'identifiant du premier joueur. + * Peut être null si le joueur n'a pas encore rejoint. + * Utilisé par Gson pour la sérialisation avec le nom "player1Id". + */ @SerializedName("player1Id") private String player1Id; - @SerializedName("player2Id") - private String player2Id; // Peut être null si en attente - // --- Getters (et Setters si nécessaire) --- - public String getGameId() { return gameId; } - public String getStatus() { return status; } - public String getPlayer1Id() { return player1Id; } - public String getPlayer2Id() { return player2Id; } + /** + * L'identifiant du deuxième joueur. + * Peut être null si le joueur n'a pas encore rejoint ou s'il s'agit d'une partie solo. + * Utilisé par Gson pour la sérialisation avec le nom "player2Id". + */ + @SerializedName("player2Id") + private String player2Id; + + /** + * Constructeur par défaut. + * Nécessaire pour certaines bibliothèques de sérialisation/désérialisation comme Gson. + */ + public GameInfo() { + // Constructeur par défaut explicite pour la clarté et la compatibilité + } + + + /** + * Récupère l'identifiant unique de la partie. + * + * @return L'identifiant de la partie. + */ + public String getGameId() { + return gameId; + } + + /** + * Récupère le statut actuel de la partie. + * + * @return Le statut de la partie (ex: "waiting", "playing", "finished"). + */ + public String getStatus() { + return status; + } + + /** + * Récupère l'identifiant du premier joueur. + * + * @return L'identifiant du joueur 1, ou null s'il n'est pas défini. + */ + public String getPlayer1Id() { + return player1Id; + } + + /** + * Récupère l'identifiant du deuxième joueur. + * + * @return L'identifiant du joueur 2, ou null s'il n'est pas défini. + */ + public String getPlayer2Id() { + return player2Id; + } } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java b/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java index 7b3a527..7de65f6 100644 --- a/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java +++ b/app/src/main/java/legion/muyue/best2048/data/GameStateResponse.java @@ -1,64 +1,218 @@ package legion.muyue.best2048.data; import com.google.gson.annotations.SerializedName; +import java.util.Arrays; +/** + * Représente l'état complet d'une partie du jeu 2048 à un instant T. + * Cette classe est typiquement utilisée pour encapsuler les données reçues + * en réponse à une requête d'état de jeu, souvent via JSON (Gson). + * Elle contient les informations sur le plateau, les scores, le joueur courant, etc. + */ public class GameStateResponse { + + /** + * L'identifiant unique de la partie. + * Utilisé par Gson pour la sérialisation avec le nom "gameId". + */ @SerializedName("gameId") private String gameId; + + /** + * La grille de jeu actuelle, représentée par un tableau 2D d'entiers. + * Chaque entier représente la valeur d'une tuile (0 pour une case vide). + * Utilisé par Gson pour la sérialisation avec le nom "board". + */ @SerializedName("board") - private int[][] board; // Plateau de jeu actuel + private int[][] board; + + /** + * Le score actuel du joueur 1. + * Utilisé par Gson pour la sérialisation avec le nom "player1Score". + */ @SerializedName("player1Score") private int player1Score; + + /** + * Le score actuel du joueur 2. + * Utilisé par Gson pour la sérialisation avec le nom "player2Score". + */ @SerializedName("player2Score") private int player2Score; - @SerializedName("currentPlayerId") // ID du joueur dont c'est le tour + + /** + * L'identifiant du joueur dont c'est actuellement le tour de jouer. + * Utilisé par Gson pour la sérialisation avec le nom "currentPlayerId". + */ + @SerializedName("currentPlayerId") private String currentPlayerId; + + /** + * Indicateur booléen signalant si la partie est terminée. + * Utilisé par Gson pour la sérialisation avec le nom "isGameOver". + */ @SerializedName("isGameOver") private boolean isGameOver; - @SerializedName("winnerId") // ID du gagnant si terminé, null sinon + + /** + * L'identifiant du joueur gagnant, si la partie est terminée et qu'il y a un gagnant. + * Peut être null si la partie n'est pas terminée ou s'il y a égalité. + * Utilisé par Gson pour la sérialisation avec le nom "winnerId". + */ + @SerializedName("winnerId") private String winnerId; + + /** + * Le statut global de la partie (par exemple, "en cours", "terminée"). + * Utilisé par Gson pour la sérialisation avec le nom "status". + */ @SerializedName("status") private String status; - @SerializedName("player1Id") // Ajoute ce champ s'il manque + + /** + * L'identifiant du joueur 1. + * Utilisé par Gson pour la sérialisation avec le nom "player1Id". + */ + @SerializedName("player1Id") private String player1Id; - @SerializedName("player2Id") // Ajoute ce champ s'il manque + + /** + * L'identifiant du joueur 2. + * Utilisé par Gson pour la sérialisation avec le nom "player2Id". + */ + @SerializedName("player2Id") private String player2Id; - @SerializedName("targetScore") // Ajoute ce champ + + /** + * Le score cible à atteindre pour gagner la partie (par exemple, 2048). + * Utilisé par Gson pour la sérialisation avec le nom "targetScore". + */ + @SerializedName("targetScore") private int targetScore; - // --- Getters --- + + /** + * Constructeur par défaut. + * Nécessaire pour certaines bibliothèques de sérialisation/désérialisation comme Gson. + */ + public GameStateResponse() { + // Constructeur par défaut explicite + } + + /** + * Récupère l'identifiant unique de la partie. + * + * @return L'identifiant de la partie. + */ public String getGameId() { return gameId; } + + /** + * Récupère la grille de jeu actuelle. + * Le tableau retourné est une référence directe à l'état interne ; + * pour éviter des modifications accidentelles, envisagez de retourner une copie. + * Exemple de copie : {@code return Arrays.stream(board).map(int[]::clone).toArray(int[][]::new);} + * + * @return Un tableau 2D d'entiers représentant le plateau de jeu. + */ public int[][] getBoard() { return board; } + + /** + * Récupère le score du joueur 1. + * + * @return Le score du joueur 1. + */ public int getPlayer1Score() { return player1Score; } + + /** + * Récupère le score du joueur 2. + * + * @return Le score du joueur 2. + */ public int getPlayer2Score() { return player2Score; } + + /** + * Récupère l'identifiant du joueur dont c'est le tour. + * + * @return L'identifiant du joueur courant. + */ public String getCurrentPlayerId() { return currentPlayerId; } + + /** + * Vérifie si la partie est terminée. + * + * @return true si la partie est terminée, false sinon. + */ public boolean isGameOver() { return isGameOver; } + + /** + * Récupère l'identifiant du joueur gagnant. + * + * @return L'identifiant du gagnant, ou null si la partie n'est pas terminée ou s'il n'y a pas de gagnant unique. + */ public String getWinnerId() { return winnerId; } + + /** + * Récupère le statut actuel de la partie. + * + * @return Le statut de la partie (ex: "playing", "finished"). + */ public String getStatus() { return status; } + + /** + * Récupère l'identifiant du joueur 1. + * + * @return L'identifiant du joueur 1. + */ public String getPlayer1Id() { return player1Id; } + + /** + * Récupère l'identifiant du joueur 2. + * + * @return L'identifiant du joueur 2. + */ public String getPlayer2Id() { return player2Id; } + + /** + * Récupère le score cible à atteindre pour gagner. + * + * @return Le score cible (par exemple, 2048). + */ public int getTargetScore() { return targetScore; } - // --- Méthode utilitaire pour obtenir le score de l'adversaire --- + + /** + * Calcule et retourne le score de l'adversaire par rapport à l'identifiant du joueur fourni. + * + * @param myActualPlayerId L'identifiant du joueur pour lequel on veut connaître le score de l'adversaire. + * @return Le score de l'adversaire si {@code myActualPlayerId} correspond à l'un des joueurs, sinon 0. Retourne 0 si {@code myActualPlayerId} est null. + */ public int getOpponentScore(String myActualPlayerId) { - if (myActualPlayerId == null) return 0; + if (myActualPlayerId == null) { + return 0; + } if (myActualPlayerId.equals(player1Id)) { - // Si je suis P1, le score de l'adversaire est P2 return player2Score; } else if (myActualPlayerId.equals(player2Id)) { - // Si je suis P2, le score de l'adversaire est P1 return player1Score; } - return 0; // Mon ID ne correspond à aucun joueur ? + return 0; } + /** + * Calcule et retourne le score du joueur correspondant à l'identifiant fourni. + * + * @param myActualPlayerId L'identifiant du joueur dont on veut connaître le score. + * @return Le score du joueur si {@code myActualPlayerId} correspond à l'un des joueurs, sinon 0. Retourne 0 si {@code myActualPlayerId} est null. + */ public int getMyScore(String myActualPlayerId) { - if (myActualPlayerId == null) return 0; + if (myActualPlayerId == null) { + return 0; + } if (myActualPlayerId.equals(player1Id)) { return player1Score; } else if (myActualPlayerId.equals(player2Id)) { return player2Score; } - return 0; // Mon ID ne correspond à aucun joueur ? + return 0; } } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java b/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java index 8cc5e3c..b277ff1 100644 --- a/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java +++ b/app/src/main/java/legion/muyue/best2048/data/MoveRequest.java @@ -1,12 +1,39 @@ package legion.muyue.best2048.data; +/** + * Représente une requête pour effectuer un mouvement dans le jeu 2048. + * Cette classe est typiquement utilisée comme un objet de transfert de données (DTO) + * pour envoyer l'action d'un joueur (la direction du mouvement) au serveur ou + * au moteur de jeu. + * Elle contient l'identifiant du joueur effectuant le mouvement et la direction choisie. + */ public class MoveRequest { - private String direction; // "UP", "DOWN", "LEFT", "RIGHT" - private String playerId; // ID du joueur qui fait le mouvement + /** + * La direction du mouvement demandé. + * Par exemple : "UP", "DOWN", "LEFT", "RIGHT". + * La casse et les valeurs exactes dépendent de la convention utilisée par le système. + * Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication. + */ + private String direction; + + /** + * L'identifiant unique du joueur qui effectue le mouvement. + * Permet au système de savoir quel joueur est à l'origine de la requête. + * Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication. + */ + private String playerId; + + /** + * Construit une nouvelle requête de mouvement. + * + * @param direction La direction souhaitée pour le mouvement (ex: "UP", "DOWN", "LEFT", "RIGHT"). + * Ne doit généralement pas être null ou vide. + * @param playerId L'identifiant du joueur effectuant la requête. + * Ne doit généralement pas être null ou vide. + */ public MoveRequest(String direction, String playerId) { this.direction = direction; this.playerId = playerId; } - // Pas besoin de getters si seulement utilisé pour l'envoi avec Gson } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/data/PlayerIdRequest.java b/app/src/main/java/legion/muyue/best2048/data/PlayerIdRequest.java index d27d2b4..20087e2 100644 --- a/app/src/main/java/legion/muyue/best2048/data/PlayerIdRequest.java +++ b/app/src/main/java/legion/muyue/best2048/data/PlayerIdRequest.java @@ -1,10 +1,27 @@ package legion.muyue.best2048.data; +/** + * Représente une requête contenant uniquement l'identifiant d'un joueur. + * Cette classe est typiquement utilisée comme un objet de transfert de données (DTO) + * simple pour les opérations où seul l'identifiant du joueur est nécessaire, + * par exemple, pour rejoindre une partie, demander des informations spécifiques + * au joueur, ou s'identifier auprès d'un service. + */ public class PlayerIdRequest { + + /** + * L'identifiant unique du joueur concerné par la requête. + * Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication. + */ private String playerId; + /** + * Construit une nouvelle requête contenant un identifiant de joueur. + * + * @param playerId L'identifiant unique du joueur. + * Ne doit généralement pas être null ou vide. + */ public PlayerIdRequest(String playerId) { this.playerId = playerId; } - // Pas besoin de getters si seulement utilisé pour l'envoi avec Gson } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/network/ApiClient.java b/app/src/main/java/legion/muyue/best2048/network/ApiClient.java index 1baedc6..8a36b63 100644 --- a/app/src/main/java/legion/muyue/best2048/network/ApiClient.java +++ b/app/src/main/java/legion/muyue/best2048/network/ApiClient.java @@ -5,43 +5,78 @@ import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; +/** + * Fournit un client Retrofit configuré pour interagir avec l'API du jeu Best2048. + * Cette classe utilise un modèle Singleton (paresseux et non strictement thread-safe dans sa forme actuelle) + * pour l'instance Retrofit, garantissant qu'une seule instance est créée et réutilisée + * pour toutes les requêtes réseau. + * La configuration inclut l'URL de base de l'API, un intercepteur pour logger les requêtes/réponses + * (niveau BODY), et un convertisseur Gson pour la sérialisation/désérialisation JSON. + */ public class ApiClient { - // URL de base de votre API serveur + /** + * L'URL de base pour l'API du jeu Best2048. + * Toutes les requêtes définies dans {@link ApiService} seront relatives à cette URL. + */ private static final String BASE_URL = "https://best2048.legion-muyue.fr/api/"; + /** + * L'instance Singleton de Retrofit. + * Initialisée paresseusement lors du premier appel à {@link #getClient()}. + * Note: L'initialisation paresseuse ici n'est pas garantie comme étant thread-safe + * dans des scénarios de haute concurrence sans synchronisation externe. + */ private static Retrofit retrofit = null; /** - * Crée et retourne une instance singleton de Retrofit configurée. - * Inclut un intercepteur pour logger les requêtes/réponses HTTP (utile pour le debug). + * Constructeur privé pour empêcher l'instanciation directe de cette classe utilitaire. + */ + private ApiClient() { + // Classe utilitaire, ne doit pas être instanciée. + } + + /** + * Récupère l'instance Singleton de Retrofit. + * Si l'instance n'existe pas encore, elle est créée et configurée avec : + *