Refactor: Nettoyage code, documentation et raffinements finaux
- **Cleanup:** Suppression logs de débogage superflus, commentaires obsolètes. Standardisation logging. - **Documentation:** Ajout/Amélioration significative des commentaires JavaDoc (Game, GameStats, MainActivity, etc.). - **Game Logic:** Refactoring des méthodes pushX en processMove. Ajout validation et helpers (isIndexValid). Amélioration robustesse (deserialize, win/loss checks). Classe rendue totalement indépendante du contexte/prefs. - **Stats:** Suppression stat 'perfectGames'. Amélioration formatage temps/moyennes. - **Notifications:** Logique de cooldown/intervalle affinée dans NotificationService. Utilisation centralisée de NotificationHelper. Gestion permission robuste. - **MainActivity:** Refactoring affichage plateau (syncBoardView avec fonds + tuiles). Animation simplifiée (apparition/fusion seulement). Gestion améliorée des états de jeu (PLAYING, WON, GAME_OVER). Logique onResume/onPause/load/save affinée. Placeholders pour Menu/Multi clarifiés. - **UI:** Layouts de dialogues standardisés avec LinearLayout. Permissions AndroidManifest nettoyées. Ajustements mineurs (marges, couleurs, strings, style police).
This commit is contained in:
parent
76d0976575
commit
bee192e335
@ -4,16 +4,6 @@
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -22,7 +12,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Best2048">
|
||||
android:theme="@style/Theme.Best2048"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -32,6 +24,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".NotificationService"
|
||||
android:enabled="true"
|
||||
|
@ -1,14 +1,15 @@
|
||||
// Fichier Game.java
|
||||
/**
|
||||
* Représente la logique métier du jeu 2048. Gère l'état du plateau de jeu,
|
||||
* les déplacements, les fusions, le score de la partie en cours, et les conditions
|
||||
* de victoire ou de défaite. Cette classe est conçue pour être indépendante du
|
||||
* framework Android (pas de dépendance au Contexte ou aux SharedPreferences).
|
||||
* Représente la logique métier du jeu 2048.
|
||||
* Gère l'état du plateau de jeu, les déplacements des tuiles, les fusions,
|
||||
* le score de la partie en cours, ainsi que les conditions de victoire ou de défaite.
|
||||
* Cette classe est conçue pour être indépendante du framework Android, ne contenant
|
||||
* aucune dépendance au Contexte Android ou aux SharedPreferences.
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting; // Pour les méthodes de test éventuelles
|
||||
import androidx.annotation.Nullable; // Ajout pour le retour de deserialize
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -16,24 +17,26 @@ import java.util.Random;
|
||||
|
||||
public class Game {
|
||||
|
||||
/** La taille du plateau de jeu (nombre de lignes/colonnes, typiquement 4x4). */
|
||||
private static final int BOARD_SIZE = 4;
|
||||
|
||||
/** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
|
||||
private int[][] board;
|
||||
/** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
|
||||
private final Random randomNumberGenerator;
|
||||
/** Score de la partie actuellement en cours. */
|
||||
private int currentScore = 0;
|
||||
/** Meilleur score global (reçu et stocké, mais non géré logiquement ici). */
|
||||
/** Meilleur score global connu par cette instance (généralement défini depuis l'extérieur). */
|
||||
private int highestScore = 0;
|
||||
/** Taille du plateau de jeu (nombre de lignes/colonnes). */
|
||||
private static final int BOARD_SIZE = 4;
|
||||
/** Indicateur si la condition de victoire (>= 2048) a été atteinte. */
|
||||
/** Indicateur si la condition de victoire (une tuile >= 2048) a été atteinte. */
|
||||
private boolean gameWon = false;
|
||||
/** Indicateur si la partie est terminée (plus de mouvements possibles). */
|
||||
/** Indicateur si la partie est terminée (plus de mouvements ou de fusions possibles). */
|
||||
private boolean gameOver = false;
|
||||
|
||||
/**
|
||||
* Constructeur pour démarrer une nouvelle partie.
|
||||
* Initialise un plateau vide, le score à 0, et ajoute deux tuiles initiales.
|
||||
* Initialise un plateau vide, met le score courant à 0, réinitialise les états
|
||||
* de victoire/défaite, et ajoute deux tuiles initiales aléatoirement.
|
||||
*/
|
||||
public Game() {
|
||||
this.randomNumberGenerator = new Random();
|
||||
@ -43,81 +46,145 @@ public class Game {
|
||||
/**
|
||||
* Constructeur pour restaurer une partie à partir d'un état sauvegardé.
|
||||
* Le meilleur score (`highestScore`) doit être défini séparément via {@link #setHighestScore(int)}.
|
||||
* Recalcule les états `gameWon` et `gameOver` en fonction du plateau fourni.
|
||||
* Recalcule les états `gameWon` et `gameOver` en fonction du plateau et du score fournis.
|
||||
* Lance une exception si le plateau fourni n'a pas les bonnes dimensions.
|
||||
*
|
||||
* @param board Le plateau de jeu restauré.
|
||||
* @param board Le plateau de jeu restauré. Doit être de dimensions BOARD_SIZE x BOARD_SIZE.
|
||||
* @param score Le score courant restauré.
|
||||
* @throws IllegalArgumentException si les dimensions du plateau fourni sont incorrectes.
|
||||
*/
|
||||
public Game(int[][] board, int score) {
|
||||
// Valider les dimensions du plateau fourni ? Pourrait être ajouté.
|
||||
this.board = board;
|
||||
public Game(@NonNull int[][] board, int score) {
|
||||
// Validation des dimensions du plateau
|
||||
if (board == null || board.length != BOARD_SIZE || board[0].length != BOARD_SIZE) {
|
||||
throw new IllegalArgumentException("Le plateau fourni n'a pas les dimensions attendues (" + BOARD_SIZE + "x" + BOARD_SIZE + ").");
|
||||
}
|
||||
this.board = board; // Attention: shallow copy si board est modifié ailleurs après coup. getBoard() est plus sûr.
|
||||
// Pour la restauration, on suppose que le tableau fourni est destiné à cet usage unique.
|
||||
this.currentScore = score;
|
||||
this.randomNumberGenerator = new Random();
|
||||
// Recalculer l'état de victoire/défaite basé sur le plateau chargé
|
||||
checkWinCondition();
|
||||
checkGameOverCondition();
|
||||
// Vérifier si la partie chargée est déjà terminée
|
||||
checkGameOverCondition(); // Important si on charge une partie déjà terminée
|
||||
}
|
||||
|
||||
// --- Getters / Setters ---
|
||||
|
||||
/**
|
||||
* Retourne la valeur de la tuile aux coordonnées spécifiées.
|
||||
* @param row Ligne (0 à BOARD_SIZE-1).
|
||||
* @param column Colonne (0 à BOARD_SIZE-1).
|
||||
* @return Valeur de la tuile, ou 0 si les coordonnées sont invalides.
|
||||
* Retourne la valeur de la tuile aux coordonnées spécifiées (ligne, colonne).
|
||||
* Les indices sont basés sur 0.
|
||||
*
|
||||
* @param row Ligne de la cellule (0 à BOARD_SIZE-1).
|
||||
* @param column Colonne de la cellule (0 à BOARD_SIZE-1).
|
||||
* @return La valeur de la tuile, ou 0 si la cellule est vide ou les coordonnées sont invalides.
|
||||
*/
|
||||
public int getCellValue(int row, int column) {
|
||||
if (isIndexValid(row, column)) {
|
||||
return this.board[row][column];
|
||||
}
|
||||
return 0; // Retourne 0 pour indice invalide
|
||||
// Log ou gestion d'erreur pourrait être ajouté si un accès invalide est critique
|
||||
return 0; // Retourne 0 pour indice invalide comme convention
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la valeur d'une tuile aux coordonnées spécifiées.
|
||||
* Ne fait rien si les coordonnées sont invalides.
|
||||
* @param row Ligne (0 à BOARD_SIZE-1).
|
||||
* @param col Colonne (0 à BOARD_SIZE-1).
|
||||
* @param value Nouvelle valeur de la tuile.
|
||||
* Utilisé principalement en interne ou pour les tests. Ne fait rien si
|
||||
* les coordonnées (ligne, colonne) sont en dehors des limites du plateau.
|
||||
*
|
||||
* @param row Ligne de la cellule (0 à BOARD_SIZE-1).
|
||||
* @param col Colonne de la cellule (0 à BOARD_SIZE-1).
|
||||
* @param value Nouvelle valeur entière pour la tuile.
|
||||
*/
|
||||
public void setCellValue(int row, int col, int value) {
|
||||
@VisibleForTesting // Indique que cette méthode est surtout pour les tests
|
||||
void setCellValue(int row, int col, int value) {
|
||||
if (isIndexValid(row, col)) {
|
||||
this.board[row][col] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Le score actuel de la partie. */
|
||||
public int getCurrentScore() { return currentScore; }
|
||||
/**
|
||||
* Retourne le score actuel de la partie en cours.
|
||||
* Le score augmente lors de la fusion de tuiles.
|
||||
*
|
||||
* @return Le score entier actuel.
|
||||
*/
|
||||
public int getCurrentScore() {
|
||||
return currentScore;
|
||||
}
|
||||
|
||||
/** @return Le meilleur score connu par cet objet (défini via setHighestScore). */
|
||||
public int getHighestScore() { return highestScore; }
|
||||
/**
|
||||
* Retourne le meilleur score connu par cette instance de jeu.
|
||||
* Cette valeur est généralement chargée depuis une sauvegarde externe et mise à jour via {@link #setHighestScore(int)}.
|
||||
*
|
||||
* @return Le meilleur score entier connu.
|
||||
*/
|
||||
public int getHighestScore() {
|
||||
return highestScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la valeur du meilleur score stockée dans cet objet Game.
|
||||
* Typiquement appelé par la classe gérant la persistance (MainActivity).
|
||||
* @param highScore Le meilleur score global à stocker.
|
||||
* Cette méthode est typiquement appelée par la classe gérant la persistance
|
||||
* (par exemple, MainActivity) après avoir chargé le meilleur score global.
|
||||
*
|
||||
* @param highScore Le nouveau meilleur score global à stocker dans cette instance.
|
||||
*/
|
||||
public void setHighestScore(int highScore) { this.highestScore = highScore; }
|
||||
|
||||
/** @return true si une tuile 2048 (ou plus) a été atteinte, false sinon. */
|
||||
public boolean isGameWon() { return gameWon; }
|
||||
|
||||
/** @return true si aucune case n'est vide ET aucun mouvement/fusion n'est possible, false sinon. */
|
||||
public boolean isGameOver() { return gameOver; }
|
||||
|
||||
/** Met à jour la valeur de gameWon si partie gagné **/
|
||||
private void setGameWon(boolean won) {this.gameWon = won;}
|
||||
|
||||
/** Met à jour la valeur de gameWon si partie gagné **/
|
||||
private void setGameOver(boolean over) {this.gameOver = over;}
|
||||
public void setHighestScore(int highScore) {
|
||||
this.highestScore = highScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une copie profonde du plateau de jeu actuel.
|
||||
* Utile pour la sérialisation ou pour éviter des modifications externes non désirées.
|
||||
* @return Une nouvelle matrice 2D représentant l'état actuel du plateau.
|
||||
* Vérifie si la condition de victoire a été atteinte (au moins une tuile avec une valeur >= 2048).
|
||||
*
|
||||
* @return true si le jeu est considéré comme gagné, false sinon.
|
||||
*/
|
||||
public boolean isGameWon() {
|
||||
return gameWon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la partie est terminée.
|
||||
* La partie est terminée si aucune case n'est vide ET aucun mouvement ou fusion n'est possible
|
||||
* dans aucune des quatre directions.
|
||||
*
|
||||
* @return true si la partie est terminée (game over), false sinon.
|
||||
*/
|
||||
public boolean isGameOver() {
|
||||
return gameOver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour l'indicateur interne de victoire du jeu.
|
||||
* Utilisé après une fusion ou lors de la restauration d'une partie.
|
||||
*
|
||||
* @param won true si la condition de victoire est atteinte, false sinon.
|
||||
*/
|
||||
private void setGameWon(boolean won) {
|
||||
this.gameWon = won;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour l'indicateur interne de fin de partie (game over).
|
||||
* Utilisé après une tentative de mouvement infructueuse ou lors de la restauration.
|
||||
*
|
||||
* @param over true si la condition de fin de partie est atteinte, false sinon.
|
||||
*/
|
||||
private void setGameOver(boolean over) {
|
||||
this.gameOver = over;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une copie profonde (deep copy) du plateau de jeu actuel.
|
||||
* Ceci est utile pour obtenir l'état du plateau sans risquer de le modifier
|
||||
* accidentellement de l'extérieur, ou pour la sérialisation.
|
||||
*
|
||||
* @return Une nouvelle matrice 2D (`int[BOARD_SIZE][BOARD_SIZE]`) représentant l'état actuel du plateau.
|
||||
*/
|
||||
@NonNull
|
||||
public int[][] getBoard() {
|
||||
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
|
||||
for(int i=0; i<BOARD_SIZE; i++) {
|
||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||
// System.arraycopy est efficace pour copier des tableaux primitifs
|
||||
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
|
||||
}
|
||||
return copy;
|
||||
@ -125,37 +192,48 @@ public class Game {
|
||||
|
||||
/**
|
||||
* (Ré)Initialise le plateau de jeu pour une nouvelle partie.
|
||||
* Remplit le plateau de zéros, réinitialise le score et les états `gameWon`/`gameOver`,
|
||||
* puis ajoute deux tuiles initiales.
|
||||
* Crée une nouvelle matrice vide (remplie de 0), réinitialise le score courant
|
||||
* et les indicateurs `gameWon`/`gameOver`, puis appelle {@link #addNewTile()}
|
||||
* deux fois pour placer les deux premières tuiles aléatoires.
|
||||
*/
|
||||
private void initializeNewBoard() {
|
||||
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice vide
|
||||
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice remplie de zéros par défaut
|
||||
this.currentScore = 0;
|
||||
this.gameWon = false;
|
||||
this.gameOver = false;
|
||||
addNewTile(); // Ajoute la première tuile
|
||||
addNewTile(); // Ajoute la seconde tuile
|
||||
// Ajoute les deux premières tuiles requises pour démarrer une partie
|
||||
addNewTile();
|
||||
addNewTile();
|
||||
}
|
||||
|
||||
// --- Logique du Jeu ---
|
||||
|
||||
/**
|
||||
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon probabilités) sur une case vide aléatoire.
|
||||
* Si le plateau est plein, cette méthode ne fait rien.
|
||||
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon les probabilités définies
|
||||
* dans {@link #generateRandomTileValue()}) sur une case vide choisie aléatoirement.
|
||||
* Si le plateau est plein (aucune case vide), cette méthode ne fait rien.
|
||||
*/
|
||||
public void addNewTile() {
|
||||
List<int[]> emptyCells = findEmptyCells();
|
||||
if (!emptyCells.isEmpty()) {
|
||||
// Choisit une cellule vide au hasard parmi celles disponibles
|
||||
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
|
||||
int value = generateRandomTileValue();
|
||||
// Place la nouvelle tuile sur le plateau logique
|
||||
// Utilise setCellValue pour la cohérence, même si l'accès direct serait possible ici
|
||||
setCellValue(randomCell[0], randomCell[1], value);
|
||||
}
|
||||
// Si emptyCells est vide, le plateau est plein, on ne peut rien ajouter.
|
||||
// La condition de Game Over sera vérifiée après la tentative de mouvement.
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve toutes les cellules vides sur le plateau.
|
||||
* @return Une liste de tableaux d'entiers `[row, col]` pour chaque cellule vide.
|
||||
* Recherche et retourne les coordonnées de toutes les cellules vides (valeur 0) sur le plateau.
|
||||
*
|
||||
* @return Une {@link List} de tableaux d'entiers `[row, col]` pour chaque cellule vide trouvée.
|
||||
* Retourne une liste vide si le plateau est plein.
|
||||
*/
|
||||
@NonNull
|
||||
private List<int[]> findEmptyCells() {
|
||||
List<int[]> emptyCells = new ArrayList<>();
|
||||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||||
@ -169,211 +247,368 @@ public class Game {
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère la valeur pour une nouvelle tuile en utilisant des probabilités prédéfinies.
|
||||
* @return La valeur (2, 4, 8, ...).
|
||||
* Génère la valeur pour une nouvelle tuile (2, 4, 8, ...) en utilisant une distribution
|
||||
* de probabilités prédéfinie. Les probabilités sont ajustées pour rendre l'apparition
|
||||
* des tuiles de faible valeur plus fréquente.
|
||||
*
|
||||
* Probabilités actuelles (approximatives) :
|
||||
* - 2: 85.40%
|
||||
* - 4: 12.00%
|
||||
* - 8: 2.00%
|
||||
* - 16: 0.50%
|
||||
* - 32: 0.05%
|
||||
* - 64: 0.03%
|
||||
* - 128: 0.01%
|
||||
* - 256: 0.01%
|
||||
*
|
||||
* @return La valeur entière (2, 4, 8, ...) de la tuile générée.
|
||||
*/
|
||||
private int generateRandomTileValue() {
|
||||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour pourcentages fins
|
||||
if (randomValue < 8540) return 2; // 85.40%
|
||||
if (randomValue < 9740) return 4; // 12.00%
|
||||
if (randomValue < 9940) return 8; // 2.00%
|
||||
if (randomValue < 9990) return 16; // 0.50%
|
||||
if (randomValue < 9995) return 32; // 0.05%
|
||||
if (randomValue < 9998) return 64; // 0.03%
|
||||
if (randomValue < 9999) return 128;// 0.01%
|
||||
return 256; // 0.01%
|
||||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour une granularité fine des pourcentages
|
||||
|
||||
// Les seuils définissent les probabilités cumulées
|
||||
if (randomValue < 8540) return 2; // 0 <= randomValue < 8540 (85.40%)
|
||||
if (randomValue < 9740) return 4; // 8540 <= randomValue < 9740 (12.00%)
|
||||
if (randomValue < 9940) return 8; // 9740 <= randomValue < 9940 (2.00%)
|
||||
if (randomValue < 9990) return 16; // 9940 <= randomValue < 9990 (0.50%)
|
||||
if (randomValue < 9995) return 32; // 9990 <= randomValue < 9995 (0.05%)
|
||||
if (randomValue < 9998) return 64; // 9995 <= randomValue < 9998 (0.03%)
|
||||
if (randomValue < 9999) return 128; // 9998 <= randomValue < 9999 (0.01%)
|
||||
return 256; // 9999 <= randomValue < 10000 (0.01%)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers le HAUT.
|
||||
* Met à jour le score interne et vérifie les états win/gameOver.
|
||||
* @return true si le plateau a été modifié, false sinon.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
*/
|
||||
public boolean pushUp() { return processMove(MoveDirection.UP); }
|
||||
public boolean pushUp() {
|
||||
return processMove(MoveDirection.UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers le BAS.
|
||||
* @return true si le plateau a été modifié, false sinon.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
*/
|
||||
public boolean pushDown() { return processMove(MoveDirection.DOWN); }
|
||||
public boolean pushDown() {
|
||||
return processMove(MoveDirection.DOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
|
||||
* @return true si le plateau a été modifié, false sinon.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
*/
|
||||
public boolean pushLeft() { return processMove(MoveDirection.LEFT); }
|
||||
public boolean pushLeft() {
|
||||
return processMove(MoveDirection.LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers la DROITE.
|
||||
* @return true si le plateau a été modifié, false sinon.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
*/
|
||||
public boolean pushRight() { return processMove(MoveDirection.RIGHT); }
|
||||
public boolean pushRight() {
|
||||
return processMove(MoveDirection.RIGHT);
|
||||
}
|
||||
|
||||
/** Énumération interne pour clarifier le traitement des mouvements. */
|
||||
/** Énumération interne pour clarifier la direction du mouvement dans {@link #processMove}. */
|
||||
private enum MoveDirection { UP, DOWN, LEFT, RIGHT }
|
||||
|
||||
/**
|
||||
* Méthode générique pour traiter un mouvement (déplacement et fusion) dans une direction donnée.
|
||||
* Contient la logique de base partagée par les méthodes pushX.
|
||||
* @param direction La direction du mouvement.
|
||||
* @return true si le plateau a été modifié, false sinon.
|
||||
* C'est le cœur de la logique de déplacement du jeu. Elle parcourt le plateau, déplace
|
||||
* les tuiles jusqu'au bout dans la direction demandée, puis gère les fusions éventuelles.
|
||||
*
|
||||
* @param direction La direction du mouvement ({@link MoveDirection}).
|
||||
* @return true si le plateau a été modifié (au moins un déplacement ou une fusion), false sinon.
|
||||
*/
|
||||
private boolean processMove(MoveDirection direction) {
|
||||
boolean boardChanged = false;
|
||||
// Itère sur l'axe perpendiculaire au mouvement
|
||||
|
||||
// Itérer sur l'axe perpendiculaire au mouvement (colonnes pour UP/DOWN, lignes pour LEFT/RIGHT)
|
||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||
boolean[] hasMerged = new boolean[BOARD_SIZE]; // Pour éviter double fusion sur l'axe de mouvement
|
||||
// Itère sur l'axe du mouvement, dans le bon sens
|
||||
// Tableau pour suivre si une tuile sur la ligne/colonne cible a déjà résulté d'une fusion
|
||||
// pendant CE mouvement, pour éviter les fusions en chaîne (ex: 2-2-4 -> 4-4 -> 8 en un seul swipe)
|
||||
boolean[] hasMerged = new boolean[BOARD_SIZE];
|
||||
|
||||
// Itérer sur l'axe du mouvement. Le sens de parcours est crucial :
|
||||
// - Pour UP/LEFT: du début vers la fin (index 1 à N-1) car les tuiles fusionnent vers les petits indices.
|
||||
// - Pour DOWN/RIGHT: de la fin vers le début (index N-2 à 0) car les tuiles fusionnent vers les grands indices.
|
||||
int start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
|
||||
int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
|
||||
int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;
|
||||
|
||||
for (int j = start; j != end; j += step) {
|
||||
int row = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? j : i;
|
||||
int col = (direction == MoveDirection.LEFT || direction == MoveDirection.RIGHT) ? j : i;
|
||||
int row, col;
|
||||
// Déterminer les coordonnées (row, col) de la tuile courante basée sur l'axe i et l'index j
|
||||
if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) {
|
||||
row = j; col = i; // Mouvement vertical, i est la colonne, j est la ligne
|
||||
} else {
|
||||
row = i; col = j; // Mouvement horizontal, i est la ligne, j est la colonne
|
||||
}
|
||||
|
||||
if (getCellValue(row, col) != 0) {
|
||||
int currentValue = getCellValue(row, col);
|
||||
|
||||
// Si la case n'est pas vide, on essaie de la déplacer/fusionner
|
||||
if (currentValue != 0) {
|
||||
int currentRow = row;
|
||||
int currentCol = col;
|
||||
|
||||
// Calcule la position cible après déplacement dans les cases vides
|
||||
int targetRow = currentRow;
|
||||
int targetRow = currentRow; // Position cible initiale
|
||||
int targetCol = currentCol;
|
||||
|
||||
// 1. Déplacement : Trouver la case la plus éloignée atteignable dans la direction du mouvement
|
||||
int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||
int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||||
|
||||
// Tant que la case suivante est valide et vide, on continue de "glisser"
|
||||
while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
|
||||
targetRow = nextRow;
|
||||
targetCol = nextCol;
|
||||
// Calculer la case suivante pour la prochaine itération de la boucle while
|
||||
nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||
nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||||
}
|
||||
|
||||
// Déplace la tuile si sa position cible est différente
|
||||
if (targetRow != currentRow || targetCol != currentCol) {
|
||||
setCellValue(targetRow, targetCol, currentValue);
|
||||
setCellValue(currentRow, currentCol, 0);
|
||||
boardChanged = true;
|
||||
}
|
||||
|
||||
// Vérifie la fusion potentielle avec la case suivante dans la direction du mouvement
|
||||
int mergeTargetRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||
int mergeTargetCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||||
// 2. Fusion : Vérifier la case immédiatement après la position cible (targetRow, targetCol)
|
||||
// La case à vérifier pour fusion est `nextRow`, `nextCol` (calculée juste avant ou après la sortie du while)
|
||||
int mergeTargetRow = nextRow;
|
||||
int mergeTargetCol = nextCol;
|
||||
// Index utilisé pour le tableau `hasMerged` (soit la ligne soit la colonne cible de la fusion)
|
||||
int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;
|
||||
|
||||
boolean merged = false;
|
||||
// Vérifier si la case de fusion potentielle est valide, a la même valeur, et n'a pas déjà fusionné
|
||||
if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
|
||||
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
|
||||
!hasMerged[mergeIndex])
|
||||
!hasMerged[mergeIndex]) // Crucial: empêche double fusion
|
||||
{
|
||||
// Fusion !
|
||||
int newValue = currentValue * 2;
|
||||
setCellValue(mergeTargetRow, mergeTargetCol, newValue);
|
||||
setCellValue(targetRow, targetCol, 0); // La tuile qui fusionne disparaît
|
||||
currentScore += newValue;
|
||||
hasMerged[mergeIndex] = true;
|
||||
boardChanged = true;
|
||||
setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible de la fusion
|
||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine de la tuile qui a bougé ET fusionné
|
||||
currentScore += newValue; // Ajoute la *nouvelle* valeur au score
|
||||
hasMerged[mergeIndex] = true; // Marque la case cible comme ayant fusionné
|
||||
boardChanged = true; // Le plateau a changé
|
||||
merged = true;
|
||||
|
||||
// Vérifier immédiatement si cette fusion a créé une tuile >= 2048
|
||||
if (newValue >= 2048) {
|
||||
setGameWon(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Déplacement final (si pas de fusion OU si la tuile a bougé avant de potentiellement fusionner)
|
||||
// Si la tuile n'a pas fusionné (elle reste où elle est ou se déplace seulement)
|
||||
// ET que sa position cible (targetRow, targetCol) est différente de sa position initiale
|
||||
if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
|
||||
setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
|
||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
||||
boardChanged = true; // Le plateau a changé
|
||||
}
|
||||
}
|
||||
// Vérifie les conditions de fin après chaque type de mouvement complet
|
||||
checkWinCondition();
|
||||
checkGameOverCondition();
|
||||
} // Fin if (currentValue != 0)
|
||||
} // Fin boucle interne (j)
|
||||
} // Fin boucle externe (i)
|
||||
|
||||
// Après avoir traité toutes les lignes/colonnes, vérifier l'état global
|
||||
// Note: checkWinCondition est déjà appelé lors d'une fusion >= 2048, mais on le refait ici par sécurité.
|
||||
checkWinCondition(); // Vérifie si 2048 a été atteint (peut être déjà fait lors d'une fusion)
|
||||
checkGameOverCondition(); // Vérifie si plus aucun mouvement n'est possible
|
||||
|
||||
return boardChanged;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Vérifie si les indices de ligne et colonne sont valides pour le plateau.
|
||||
* @param row Ligne.
|
||||
* @param col Colonne.
|
||||
* @return true si les indices sont dans les limites [0, BOARD_SIZE-1].
|
||||
* Vérifie si les indices de ligne et colonne fournis sont valides pour le plateau de jeu actuel.
|
||||
*
|
||||
* @param row L'indice de ligne à vérifier.
|
||||
* @param col L'indice de colonne à vérifier.
|
||||
* @return true si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, false sinon.
|
||||
*/
|
||||
private boolean isIndexValid(int row, int col) {
|
||||
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sérialise l'état essentiel du jeu (plateau et score courant) pour sauvegarde.
|
||||
* Format: "val,val,...,val,score"
|
||||
* @return Chaîne représentant l'état du jeu.
|
||||
* Sérialise l'état essentiel du jeu (plateau et score courant) en une chaîne de caractères.
|
||||
* Le format actuel est simple : toutes les valeurs du plateau séparées par des virgules,
|
||||
* suivies par le score courant, également séparé par une virgule.
|
||||
* Exemple pour un plateau 2x2 : "2,0,4,8,12" (où 12 est le score).
|
||||
* <p>
|
||||
* NOTE : Ce format est simple mais peut être fragile si la structure du jeu
|
||||
* ou les données à sauvegarder évoluent. Un format comme JSON pourrait offrir plus de flexibilité.
|
||||
* </p>
|
||||
*
|
||||
* @return Une chaîne non nulle représentant l'état sérialisé du jeu.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||||
for (int col = 0; col < BOARD_SIZE; col++) { sb.append(board[row][col]).append(","); }
|
||||
for (int col = 0; col < BOARD_SIZE; col++) {
|
||||
sb.append(board[row][col]).append(",");
|
||||
}
|
||||
}
|
||||
// Ajoute le score à la fin, séparé par une virgule
|
||||
sb.append(currentScore);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un objet Game à partir de sa représentation sérialisée (plateau + score).
|
||||
* Le meilleur score doit être défini séparément après la création.
|
||||
* @param serializedState La chaîne issue de {@link #toString()}.
|
||||
* @return Une nouvelle instance de Game, ou null en cas d'erreur de format.
|
||||
* Crée (désérialise) un objet {@code Game} à partir de sa représentation sous forme de chaîne,
|
||||
* telle que générée par {@link #toString()}.
|
||||
* Le meilleur score (`highestScore`) n'est pas inclus dans la chaîne et doit être
|
||||
* défini séparément sur l'objet retourné via {@link #setHighestScore(int)}.
|
||||
* <p>
|
||||
* NOTE : Cette méthode dépend du format spécifique généré par {@code toString()}.
|
||||
* </p>
|
||||
*
|
||||
* @param serializedState La chaîne représentant l'état du jeu (plateau + score courant).
|
||||
* @return Une nouvelle instance de {@code Game} si la désérialisation réussit,
|
||||
* ou {@code null} si la chaîne est invalide, vide, ou a un format incorrect
|
||||
* (mauvais nombre d'éléments, valeur non entière).
|
||||
*/
|
||||
public static Game deserialize(String serializedState) {
|
||||
if (serializedState == null || serializedState.isEmpty()) return null;
|
||||
String[] values = serializedState.split(",");
|
||||
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; // +1 pour le score
|
||||
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0;
|
||||
try {
|
||||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||||
for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); }
|
||||
@Nullable
|
||||
public static Game deserialize(@Nullable String serializedState) {
|
||||
if (serializedState == null || serializedState.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int score = Integer.parseInt(values[index]); // Le dernier élément est le score
|
||||
String[] values = serializedState.split(",");
|
||||
// Vérifie si le nombre d'éléments correspond à la taille du plateau + 1 (pour le score)
|
||||
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
|
||||
int index = 0;
|
||||
try {
|
||||
// Remplit le plateau à partir des valeurs de la chaîne
|
||||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||||
for (int col = 0; col < BOARD_SIZE; col++) {
|
||||
newBoard[row][col] = Integer.parseInt(values[index++]);
|
||||
}
|
||||
}
|
||||
// Le dernier élément est le score
|
||||
int score = Integer.parseInt(values[index]);
|
||||
|
||||
// Crée une nouvelle instance de Game avec les données désérialisées
|
||||
// Le constructeur recalculera gameWon et gameOver.
|
||||
return new Game(newBoard, score);
|
||||
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; }
|
||||
|
||||
} catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
|
||||
// En cas d'erreur de format (valeur non entière, problème d'indice, ou dimension plateau incorrecte dans constructeur)
|
||||
// Log l'erreur pourrait être utile pour le débogage
|
||||
System.err.println("Erreur lors de la désérialisation de l'état du jeu: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
|
||||
* Met à jour l'état interne `gameWon`.
|
||||
* Vérifie si la condition de victoire (au moins une tuile avec une valeur >= 2048)
|
||||
* est actuellement atteinte sur le plateau. Met à jour l'état interne `gameWon`.
|
||||
* Cette vérification s'arrête dès qu'une tuile gagnante est trouvée ou si le jeu
|
||||
* est déjà marqué comme gagné.
|
||||
*/
|
||||
private void checkWinCondition() {
|
||||
if (!gameWon) {
|
||||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c) >= 2048) {
|
||||
setGameWon(true); return;
|
||||
// Si le jeu est déjà marqué comme gagné, pas besoin de revérifier
|
||||
if (gameWon) {
|
||||
return;
|
||||
}
|
||||
// Parcours le plateau à la recherche d'une tuile >= 2048
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
if (getCellValue(r, c) >= 2048) {
|
||||
setGameWon(true); // Met à jour l'état interne
|
||||
return; // Sort dès qu'une condition de victoire est trouvée
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si la boucle se termine sans trouver de tuile >= 2048, gameWon reste false (ou son état précédent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la condition de fin de partie est atteinte (plateau plein ET aucun mouvement possible).
|
||||
* Vérifie si la condition de fin de partie (Game Over) est atteinte.
|
||||
* Cela se produit si le plateau est plein (pas de case vide) ET si aucune
|
||||
* fusion n'est possible entre des tuiles adjacentes (horizontalement ou verticalement).
|
||||
* Met à jour l'état interne `gameOver`.
|
||||
*/
|
||||
private void checkGameOverCondition() {
|
||||
if (hasEmptyCell()) { setGameOver(false); return; } // Pas game over si case vide
|
||||
// Vérifie s'il existe au moins une fusion possible
|
||||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) {
|
||||
int current = getCellValue(r,c);
|
||||
if ((r>0 && getCellValue(r-1,c)==current) || (r<BOARD_SIZE-1 && getCellValue(r+1,c)==current) ||
|
||||
(c>0 && getCellValue(r,c-1)==current) || (c<BOARD_SIZE-1 && getCellValue(r,c+1)==current)) {
|
||||
setGameOver(false); return; // Fusion possible -> pas game over
|
||||
// Si le jeu est déjà terminé, pas besoin de revérifier
|
||||
if (gameOver) {
|
||||
return;
|
||||
}
|
||||
// Si il y a au moins une case vide, le jeu ne peut pas être terminé
|
||||
if (hasEmptyCell()) {
|
||||
setGameOver(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Le plateau est plein. Vérifier s'il existe au moins une fusion possible.
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
int current = getCellValue(r, c);
|
||||
// Vérifie le voisin du HAUT (si existe)
|
||||
if (r > 0 && getCellValue(r - 1, c) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers le haut
|
||||
}
|
||||
// Vérifie le voisin du BAS (si existe)
|
||||
if (r < BOARD_SIZE - 1 && getCellValue(r + 1, c) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers le bas
|
||||
}
|
||||
// Vérifie le voisin de GAUCHE (si existe)
|
||||
if (c > 0 && getCellValue(r, c - 1) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers la gauche
|
||||
}
|
||||
// Vérifie le voisin de DROITE (si existe)
|
||||
if (c < BOARD_SIZE - 1 && getCellValue(r, c + 1) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers la droite
|
||||
}
|
||||
}
|
||||
setGameOver(true); // Aucune case vide et aucune fusion -> game over
|
||||
}
|
||||
|
||||
// Si on arrive ici, le plateau est plein ET aucune fusion adjacente n'est possible.
|
||||
setGameOver(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le plateau contient au moins une case vide (valeur 0).
|
||||
* @return true si une case vide existe, false sinon.
|
||||
* Vérifie rapidement si le plateau contient au moins une case vide (valeur 0).
|
||||
* Utilisé principalement par {@link #checkGameOverCondition()}.
|
||||
*
|
||||
* @return true s'il existe au moins une case vide, false si le plateau est plein.
|
||||
*/
|
||||
private boolean hasEmptyCell() {
|
||||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c)==0) return true;
|
||||
return false;
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
if (getCellValue(r, c) == 0) {
|
||||
return true; // Sort dès qu'une case vide est trouvée
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // Aucune case vide trouvée après avoir parcouru tout le plateau
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la valeur de la plus haute tuile présente sur le plateau.
|
||||
* @return La valeur maximale trouvée, ou 0 si le plateau est vide.
|
||||
* Retourne la valeur de la plus haute tuile (la plus grande valeur numérique)
|
||||
* actuellement présente sur le plateau de jeu.
|
||||
*
|
||||
* @return La valeur maximale trouvée sur le plateau, ou 0 si le plateau est vide.
|
||||
*/
|
||||
public int getHighestTileValue() {
|
||||
int maxTile = 0;
|
||||
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (board[r][c] > maxTile) maxTile = board[r][c];
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
if (board[r][c] > maxTile) {
|
||||
maxTile = board[r][c];
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxTile;
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
// Fichier GameStats.java
|
||||
/**
|
||||
* Gère la collecte, la persistance (via SharedPreferences) et l'accès
|
||||
* aux statistiques du jeu 2048, pour les modes solo et multijoueur (si applicable).
|
||||
* 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;
|
||||
|
||||
@ -13,9 +14,17 @@ import java.util.concurrent.TimeUnit;
|
||||
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. */
|
||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||
private static final String HIGH_SCORE_KEY = "high_score"; // Clé partagée avec Game/MainActivity
|
||||
// Clés spécifiques aux statistiques
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static final String HIGH_SCORE_KEY = "high_score";
|
||||
|
||||
// Clés spécifiques aux statistiques persistantes
|
||||
private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed";
|
||||
private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted";
|
||||
private static final String STATS_TOTAL_MOVES = "totalMoves";
|
||||
@ -23,10 +32,11 @@ public class GameStats {
|
||||
private static final String STATS_TOTAL_MERGES = "totalMerges";
|
||||
private static final String STATS_HIGHEST_TILE = "highestTile";
|
||||
private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached";
|
||||
private static final String STATS_PERFECT_GAMES = "perfectGames";
|
||||
// private static final String STATS_PERFECT_GAMES = "perfectGames"; // Clé supprimée car concept non défini
|
||||
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
|
||||
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
|
||||
// ... (autres clés stats) ...
|
||||
|
||||
// Clés pour les statistiques multijoueur (fonctionnalité future)
|
||||
private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon";
|
||||
private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed";
|
||||
private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak";
|
||||
@ -37,94 +47,133 @@ public class GameStats {
|
||||
|
||||
|
||||
// --- Champs de Statistiques ---
|
||||
// Générales & Solo
|
||||
private int totalGamesPlayed;
|
||||
private int totalGamesStarted;
|
||||
private int totalMoves;
|
||||
private long totalPlayTimeMs;
|
||||
private int totalMerges;
|
||||
private int highestTile;
|
||||
private int numberOfTimesObjectiveReached; // Nombre de victoires (>= 2048)
|
||||
private int perfectGames; // Concept non défini ici
|
||||
private long bestWinningTimeMs;
|
||||
private long worstWinningTimeMs;
|
||||
private int overallHighScore; // Meilleur score global
|
||||
|
||||
// Partie en cours (non persistées telles quelles)
|
||||
// Générales & Solo
|
||||
/** Nombre total de parties terminées (victoire ou défaite). */
|
||||
private int totalGamesPlayed;
|
||||
/** Nombre total de parties démarrées (via bouton "Nouvelle Partie"). */
|
||||
private int totalGamesStarted;
|
||||
/** Nombre total de mouvements (swipes valides) effectués dans toutes les parties. */
|
||||
private int totalMoves;
|
||||
/** Temps total passé à jouer (en millisecondes) sur toutes les parties. */
|
||||
private long totalPlayTimeMs;
|
||||
/** Nombre total de fusions de tuiles effectuées dans toutes les parties. */
|
||||
private int totalMerges;
|
||||
/** Valeur de la plus haute tuile atteinte globalement dans toutes les parties. */
|
||||
private int highestTile;
|
||||
/** Nombre de fois où l'objectif (atteindre 2048 ou plus) a été atteint. */
|
||||
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). */
|
||||
private long bestWinningTimeMs;
|
||||
/** Pire temps (le plus long, en ms) pour atteindre l'objectif (>= 2048). */
|
||||
private long worstWinningTimeMs;
|
||||
/**
|
||||
* Meilleur score global atteint. Persisté via HIGH_SCORE_KEY.
|
||||
* Synchronisé avec MainActivity/Game lors du chargement/sauvegarde.
|
||||
*/
|
||||
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. */
|
||||
private int currentMoves;
|
||||
/** Timestamp (en ms) du début de la partie actuelle. Réinitialisé à chaque nouvelle partie ou reprise. */
|
||||
private long currentGameStartTimeMs;
|
||||
/** Nombre de fusions effectuées dans la partie actuelle. */
|
||||
private int mergesThisGame;
|
||||
|
||||
// Multijoueur
|
||||
// Multijoueur (placeholders pour fonctionnalité future)
|
||||
/** Nombre de parties multijoueur gagnées. */
|
||||
private int multiplayerGamesWon;
|
||||
/** Nombre de parties multijoueur jouées (terminées). */
|
||||
private int multiplayerGamesPlayed;
|
||||
/** Plus longue série de victoires consécutives en multijoueur. */
|
||||
private int multiplayerBestWinningStreak;
|
||||
/** Score total accumulé dans toutes les parties multijoueur. */
|
||||
private long multiplayerTotalScore;
|
||||
/** Temps total passé dans les parties multijoueur (en ms). */
|
||||
private long multiplayerTotalTimeMs;
|
||||
/** Nombre total de défaites en multijoueur. */
|
||||
private int totalMultiplayerLosses;
|
||||
/** Meilleur score atteint dans une seule partie multijoueur. */
|
||||
private int multiplayerHighestScore;
|
||||
|
||||
/** Contexte nécessaire pour accéder aux SharedPreferences. */
|
||||
/** Contexte applicatif nécessaire pour accéder aux SharedPreferences. */
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructeur. Initialise l'objet et charge immédiatement les statistiques
|
||||
* depuis les SharedPreferences.
|
||||
* @param context Contexte de l'application.
|
||||
* Constructeur de GameStats.
|
||||
* Initialise l'objet et charge immédiatement les statistiques persistantes
|
||||
* depuis les SharedPreferences via {@link #loadStats()}.
|
||||
*
|
||||
* @param context Contexte de l'application (Activity ou Application). Utilise `getApplicationContext()` pour éviter les fuites de mémoire.
|
||||
*/
|
||||
public GameStats(Context context) {
|
||||
this.context = context.getApplicationContext(); // Utilise le contexte applicatif
|
||||
loadStats();
|
||||
this.context = context.getApplicationContext(); // Important: utiliser le contexte applicatif
|
||||
loadStats(); // Charge les statistiques dès l'initialisation
|
||||
}
|
||||
|
||||
// --- Persistance (SharedPreferences) ---
|
||||
|
||||
/**
|
||||
* Charge toutes les statistiques persistantes depuis les SharedPreferences.
|
||||
* Appelé par le constructeur.
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
// ... (chargement de toutes les autres clés persistantes) ...
|
||||
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
|
||||
totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0);
|
||||
totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0L);
|
||||
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
|
||||
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
|
||||
numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 0);
|
||||
perfectGames = prefs.getInt(STATS_PERFECT_GAMES, 0);
|
||||
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme défaut pour 'best'
|
||||
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 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)
|
||||
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
|
||||
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
|
||||
multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 0);
|
||||
multiplayerTotalScore = prefs.getLong(STATS_MP_TOTAL_SCORE, 0);
|
||||
multiplayerTotalTimeMs = prefs.getLong(STATS_MP_TOTAL_TIME_MS, 0);
|
||||
multiplayerTotalScore = prefs.getLong(STATS_MP_TOTAL_SCORE, 0L);
|
||||
multiplayerTotalTimeMs = prefs.getLong(STATS_MP_TOTAL_TIME_MS, 0L);
|
||||
totalMultiplayerLosses = prefs.getInt(STATS_MP_LOSSES, 0);
|
||||
multiplayerHighestScore = prefs.getInt(STATS_MP_HIGH_SCORE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde toutes les statistiques persistantes dans les SharedPreferences.
|
||||
* Appelé typiquement dans `onPause` de l'activité.
|
||||
* 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).
|
||||
*/
|
||||
public void saveStats() {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt(HIGH_SCORE_KEY, overallHighScore); // Sauvegarde le HS global
|
||||
|
||||
// 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);
|
||||
// ... (sauvegarde de toutes les autres clés persistantes) ...
|
||||
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
|
||||
editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs);
|
||||
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
|
||||
editor.putInt(STATS_HIGHEST_TILE, highestTile);
|
||||
editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached);
|
||||
editor.putInt(STATS_PERFECT_GAMES, perfectGames);
|
||||
// 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)
|
||||
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
|
||||
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
|
||||
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
|
||||
@ -133,24 +182,28 @@ public class GameStats {
|
||||
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
|
||||
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
|
||||
|
||||
editor.apply(); // Applique les changements de manière asynchrone
|
||||
// Applique les changements de manière asynchrone
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
// --- Méthodes de Mise à Jour des Statistiques ---
|
||||
|
||||
/**
|
||||
* Doit être appelée au début de chaque nouvelle partie.
|
||||
* Incrémente le compteur de parties démarrées et réinitialise les stats de la partie en cours.
|
||||
* 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`).
|
||||
*/
|
||||
public void startGame() {
|
||||
totalGamesStarted++;
|
||||
currentMoves = 0;
|
||||
mergesThisGame = 0;
|
||||
currentGameStartTimeMs = System.currentTimeMillis();
|
||||
currentGameStartTimeMs = System.currentTimeMillis(); // Enregistre le temps de début
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un mouvement réussi (qui a modifié le plateau).
|
||||
* 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`).
|
||||
*/
|
||||
public void recordMove() {
|
||||
currentMoves++;
|
||||
@ -159,7 +212,10 @@ public class GameStats {
|
||||
|
||||
/**
|
||||
* Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
|
||||
* @param numberOfMerges Le nombre de fusions (si connu, sinon 1 par défaut).
|
||||
* Incrémente le compteur de fusions pour la partie en cours (`mergesThisGame`)
|
||||
* et le compteur total de fusions (`totalMerges`).
|
||||
*
|
||||
* @param numberOfMerges Le nombre de fusions distinctes réalisées lors du dernier mouvement (typiquement 1 ou plus).
|
||||
*/
|
||||
public void recordMerge(int numberOfMerges) {
|
||||
if (numberOfMerges > 0) {
|
||||
@ -169,47 +225,71 @@ public class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la statistique de la plus haute tuile atteinte globalement.
|
||||
* @param tileValue La valeur de la tuile la plus haute de la partie en cours.
|
||||
* 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.
|
||||
*
|
||||
* @param currentHighestTileValue La valeur de la tuile la plus haute sur le plateau à l'instant T.
|
||||
*/
|
||||
public void updateHighestTile(int tileValue) {
|
||||
if (tileValue > this.highestTile) {
|
||||
this.highestTile = tileValue;
|
||||
public void updateHighestTile(int currentHighestTileValue) {
|
||||
if (currentHighestTileValue > this.highestTile) {
|
||||
this.highestTile = currentHighestTileValue;
|
||||
// La sauvegarde se fera via saveStats() globalement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une victoire et met à jour les temps associés.
|
||||
* @param timeTakenMs Temps écoulé pour cette partie gagnante.
|
||||
* 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.
|
||||
*
|
||||
* @param timeTakenMs Temps écoulé (en millisecondes) pour terminer cette partie gagnante.
|
||||
*/
|
||||
public void recordWin(long timeTakenMs) {
|
||||
numberOfTimesObjectiveReached++;
|
||||
if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; }
|
||||
if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; }
|
||||
endGame(timeTakenMs); // Finalise aussi le temps total et parties jouées
|
||||
if (timeTakenMs < bestWinningTimeMs) {
|
||||
bestWinningTimeMs = timeTakenMs;
|
||||
}
|
||||
// Utiliser >= pour le pire temps permet de l'enregistrer même si c'est le premier
|
||||
if (timeTakenMs >= worstWinningTimeMs) {
|
||||
worstWinningTimeMs = timeTakenMs;
|
||||
}
|
||||
endGame(timeTakenMs); // Finalise aussi le temps total et le compteur de parties jouées
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une défaite.
|
||||
* 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.
|
||||
*/
|
||||
public void recordLoss() {
|
||||
// Calcule le temps écoulé avant de finaliser
|
||||
endGame(System.currentTimeMillis() - currentGameStartTimeMs);
|
||||
// Calcule le temps écoulé pour cette partie avant de la finaliser
|
||||
long timeTakenMs = System.currentTimeMillis() - currentGameStartTimeMs;
|
||||
endGame(timeTakenMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
|
||||
* @param timeTakenMs Temps total de la partie terminée.
|
||||
* Incrémente le compteur de parties jouées (`totalGamesPlayed`) et ajoute le temps
|
||||
* de la partie terminée au temps de jeu total (`totalPlayTimeMs`).
|
||||
*
|
||||
* @param timeTakenMs Temps total (en millisecondes) de la partie qui vient de se terminer.
|
||||
*/
|
||||
public void endGame(long timeTakenMs) {
|
||||
private void endGame(long timeTakenMs) {
|
||||
totalGamesPlayed++;
|
||||
addPlayTime(timeTakenMs);
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une durée (en ms) au temps de jeu total enregistré.
|
||||
* Typiquement appelé dans `onPause`.
|
||||
* @param durationMs Durée à ajouter.
|
||||
* 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`.
|
||||
*
|
||||
* @param durationMs Durée positive (en millisecondes) à ajouter au temps total.
|
||||
*/
|
||||
public void addPlayTime(long durationMs) {
|
||||
if (durationMs > 0) {
|
||||
@ -218,8 +298,10 @@ public class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise toutes les statistiques (solo, multijoueur, high score global)
|
||||
* à leurs valeurs par défaut.
|
||||
* 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()}.
|
||||
*/
|
||||
public void resetStats() {
|
||||
// Réinitialise toutes les variables membres à 0 ou valeur initiale
|
||||
@ -227,80 +309,158 @@ public class GameStats {
|
||||
totalGamesPlayed = 0;
|
||||
totalGamesStarted = 0;
|
||||
totalMoves = 0;
|
||||
totalPlayTimeMs = 0;
|
||||
totalPlayTimeMs = 0L;
|
||||
totalMerges = 0;
|
||||
highestTile = 0;
|
||||
numberOfTimesObjectiveReached = 0;
|
||||
perfectGames = 0;
|
||||
// perfectGames = 0; // Supprimé
|
||||
bestWinningTimeMs = Long.MAX_VALUE;
|
||||
worstWinningTimeMs = 0;
|
||||
worstWinningTimeMs = 0L;
|
||||
|
||||
// Réinitialise les stats multijoueur (placeholders)
|
||||
multiplayerGamesWon = 0;
|
||||
multiplayerGamesPlayed = 0;
|
||||
multiplayerBestWinningStreak = 0;
|
||||
multiplayerTotalScore = 0;
|
||||
multiplayerTotalTimeMs = 0;
|
||||
multiplayerTotalScore = 0L;
|
||||
multiplayerTotalTimeMs = 0L;
|
||||
totalMultiplayerLosses = 0;
|
||||
multiplayerHighestScore = 0;
|
||||
|
||||
// Réinitialise les stats de la partie en cours (au cas où)
|
||||
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.
|
||||
|
||||
// Sauvegarde immédiatement les statistiques réinitialisées
|
||||
saveStats();
|
||||
}
|
||||
|
||||
// --- Getters pour l'affichage ---
|
||||
|
||||
/** @return Le nombre total de parties terminées (victoire ou défaite). */
|
||||
public int getTotalGamesPlayed() { return totalGamesPlayed; }
|
||||
/** @return Le nombre total de parties démarrées. */
|
||||
public int getTotalGamesStarted() { return totalGamesStarted; }
|
||||
/** @return Le nombre total de mouvements valides effectués. */
|
||||
public int getTotalMoves() { return totalMoves; }
|
||||
/** @return Le nombre de mouvements dans la partie en cours. */
|
||||
public int getCurrentMoves() { return currentMoves; }
|
||||
/** @return Le temps total de jeu accumulé (en ms). */
|
||||
public long getTotalPlayTimeMs() { return totalPlayTimeMs; }
|
||||
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } // Utile pour calcul durée en cours
|
||||
/** @return Le timestamp (en ms) du début de la partie en cours. Utile pour calculer la durée en temps réel. */
|
||||
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; }
|
||||
/** @return Le nombre de fusions dans la partie en cours. */
|
||||
public int getMergesThisGame() { return mergesThisGame; }
|
||||
/** @return Le nombre total de fusions effectuées. */
|
||||
public int getTotalMerges() { return totalMerges; }
|
||||
/** @return La valeur de la plus haute tuile atteinte globalement. */
|
||||
public int getHighestTile() { return highestTile; }
|
||||
/** @return Le nombre de fois où l'objectif (>= 2048) a été atteint. */
|
||||
public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; }
|
||||
public int getPerfectGames() { return perfectGames; }
|
||||
// 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. */
|
||||
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
|
||||
/** @return Le pire temps (le plus long, en ms) pour gagner une partie, ou 0 si aucune victoire. */
|
||||
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
|
||||
public int getOverallHighScore() { return overallHighScore; } // Getter pour HS global
|
||||
// Getters Multiplayer
|
||||
/** @return Le meilleur score global enregistré. */
|
||||
public int getOverallHighScore() { return overallHighScore; }
|
||||
|
||||
// Getters Multiplayer (fonctionnalité future)
|
||||
/** @return Nombre de parties multijoueur gagnées. */
|
||||
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
|
||||
/** @return Nombre de parties multijoueur jouées. */
|
||||
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
|
||||
/** @return Meilleure série de victoires consécutives en multijoueur. */
|
||||
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
|
||||
/** @return Score total accumulé en multijoueur. */
|
||||
public long getMultiplayerTotalScore() { return multiplayerTotalScore; }
|
||||
/** @return Temps total passé en multijoueur (en ms). */
|
||||
public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; }
|
||||
/** @return Nombre total de défaites en multijoueur. */
|
||||
public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; }
|
||||
/** @return Meilleur score atteint dans une partie multijoueur. */
|
||||
public int getMultiplayerHighestScore() { return multiplayerHighestScore; }
|
||||
|
||||
// --- Setters ---
|
||||
/** Met à jour la valeur interne du high score global. */
|
||||
public void setHighestScore(int highScore) {
|
||||
// Met à jour si la nouvelle valeur est meilleure
|
||||
if (highScore > this.overallHighScore) {
|
||||
this.overallHighScore = highScore;
|
||||
// La sauvegarde se fait via saveStats() globalement
|
||||
}
|
||||
}
|
||||
/** Définit le timestamp de début de la partie en cours. */
|
||||
public void setCurrentGameStartTimeMs(long timeMs) { this.currentGameStartTimeMs = timeMs; }
|
||||
|
||||
// --- Méthodes Calculées ---
|
||||
/** @return Temps moyen par partie terminée en millisecondes. */
|
||||
public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; }
|
||||
/** @return Score moyen par partie multijoueur terminée. */
|
||||
public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; }
|
||||
/** @return Temps moyen par partie multijoueur terminée en millisecondes. */
|
||||
public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; }
|
||||
|
||||
/**
|
||||
* Formate une durée en millisecondes en chaîne "hh:mm:ss" ou "mm:ss".
|
||||
* @param milliseconds Durée en millisecondes.
|
||||
* @return Chaîne de temps formatée.
|
||||
* 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()}.
|
||||
*
|
||||
* @param highScore Le nouveau score à potentiellement définir comme meilleur score.
|
||||
*/
|
||||
@SuppressLint("DefaultLocale")
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param timeMs Le timestamp en millisecondes.
|
||||
*/
|
||||
public void setCurrentGameStartTimeMs(long timeMs) {
|
||||
this.currentGameStartTimeMs = timeMs;
|
||||
}
|
||||
|
||||
// --- Méthodes Calculées ---
|
||||
|
||||
/**
|
||||
* Calcule le temps moyen passé par partie terminée (solo).
|
||||
*
|
||||
* @return Le temps moyen par partie en millisecondes, ou 0 si aucune partie n'a été terminée.
|
||||
*/
|
||||
public long getAverageGameTimeMs() {
|
||||
return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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).
|
||||
*
|
||||
* @return Le temps moyen par partie multijoueur 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".
|
||||
*
|
||||
* @param milliseconds Durée totale en millisecondes.
|
||||
* @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;
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60;
|
||||
if (hours > 0) { return String.format("%02d:%02d:%02d", hours, minutes, seconds); }
|
||||
else { return String.format("%02d:%02d", minutes, seconds); }
|
||||
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
|
||||
|
||||
if (hours > 0) {
|
||||
// Format avec heures si nécessaire
|
||||
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
} else {
|
||||
// Format minutes:secondes sinon
|
||||
return String.format("%02d:%02d", minutes, seconds);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,9 @@
|
||||
// Fichier NotificationHelper.java
|
||||
/**
|
||||
* Classe utilitaire centralisant la logique de création et d'affichage des notifications
|
||||
* pour l'application Best 2048.
|
||||
* Gère également la création du canal de notification nécessaire pour Android 8.0 (API 26) et supérieur.
|
||||
* Utilise {@link NotificationCompat} pour assurer la compatibilité avec les anciennes versions d'Android.
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
@ -6,86 +11,143 @@ import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager; // Import pour PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build;
|
||||
import android.util.Log; // Pour le logging des erreurs/warnings
|
||||
|
||||
import androidx.annotation.NonNull; // Pour marquer les paramètres non-null
|
||||
import androidx.core.app.ActivityCompat; // Pour checkSelfPermission (remplace ContextCompat ici)
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat; // Pour checkSelfPermission
|
||||
// import androidx.core.content.ContextCompat; // Peut être utilisé mais ActivityCompat est plus direct pour la permission ici
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour simplifier la création et l'affichage des notifications
|
||||
* et la gestion du canal de notification pour l'application Best 2048.
|
||||
*/
|
||||
public class NotificationHelper {
|
||||
|
||||
/** Identifiant unique du canal de notification pour cette application. */
|
||||
public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé avant
|
||||
/**
|
||||
* 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+.
|
||||
*/
|
||||
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";
|
||||
|
||||
/**
|
||||
* Crée le canal de notification requis pour Android 8.0 (API 26) et supérieur.
|
||||
* Cette méthode est idempotente (l'appeler plusieurs fois n'a pas d'effet négatif).
|
||||
* Doit être appelée avant d'afficher la première notification sur API 26+.
|
||||
* 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+.
|
||||
*
|
||||
* @param context Contexte applicatif.
|
||||
* @param context Le contexte applicatif ({@code Context}) utilisé pour accéder aux services système.
|
||||
*/
|
||||
public static void createNotificationChannel(Context context) {
|
||||
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);
|
||||
int importance = NotificationManager.IMPORTANCE_DEFAULT; // Importance par défaut
|
||||
// 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;
|
||||
|
||||
// Crée l'objet NotificationChannel
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
// Enregistre le canal auprès du système.
|
||||
// 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.
|
||||
* Vérifie la permission POST_NOTIFICATIONS sur Android 13+ avant d'essayer d'afficher.
|
||||
* 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.
|
||||
*
|
||||
* @param context Contexte (peut être une Activity ou un Service).
|
||||
* @param title Titre de la notification.
|
||||
* @param message Contenu texte de la notification.
|
||||
* @param notificationId ID unique pour cette notification (permet de la mettre à jour ou l'annuler).
|
||||
* @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.
|
||||
*/
|
||||
public static void showNotification(Context context, String title, String message, int notificationId) {
|
||||
// Vérification de la permission pour Android 13+
|
||||
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) {
|
||||
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
// Si la permission n'est pas accordée, ne pas tenter d'afficher la notification.
|
||||
// L'application devrait idéalement gérer la demande de permission avant d'appeler cette méthode
|
||||
// si elle sait que l'utilisateur a activé les notifications dans les paramètres.
|
||||
System.err.println("Permission POST_NOTIFICATIONS manquante. Notification non affichée.");
|
||||
// Utilise ActivityCompat.checkSelfPermission pour vérifier la permission
|
||||
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+.");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "API < 33, permission POST_NOTIFICATIONS non requise pour afficher.");
|
||||
}
|
||||
|
||||
// Intent pour ouvrir MainActivity au clic
|
||||
// --- Création de l'Intent pour le clic sur la notification ---
|
||||
// Ouvre MainActivity lorsque l'utilisateur clique sur la notification.
|
||||
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)
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags);
|
||||
// Flags pour le PendingIntent (requis : IMMUTABLE sur API 31+)
|
||||
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 pour la compatibilité
|
||||
// --- Construction de la notification via NotificationCompat ---
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Votre icône de notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité normale
|
||||
.setContentIntent(pendingIntent) // Action au clic
|
||||
.setAutoCancel(true); // Ferme la notification après le clic
|
||||
.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é)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); // Visible sur l'écran de verrouillage
|
||||
|
||||
// Affichage de la notification
|
||||
// --- Affichage de la notification ---
|
||||
// Utilise NotificationManagerCompat pour la compatibilité
|
||||
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 survenir même avec la vérification ci-dessus dans certains cas limites
|
||||
System.err.println("Erreur de sécurité lors de l'affichage de la notification : " + e.getMessage());
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,22 @@
|
||||
// Fichier NotificationService.java
|
||||
/**
|
||||
* 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é.
|
||||
*
|
||||
* <p><strong>AVERTISSEMENT IMPORTANT :</strong> Ce service utilise un {@link android.os.Handler}
|
||||
* pour planifier les tâches répétitives. Cette approche est simple mais <strong>NON FIABLE</strong>
|
||||
* 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). </p>
|
||||
*
|
||||
* <p><strong>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}.</strong>
|
||||
* 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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import android.app.Service;
|
||||
@ -7,129 +25,227 @@ 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;
|
||||
|
||||
/**
|
||||
* Service exécuté en arrière-plan pour envoyer des notifications périodiques
|
||||
* (rappel de meilleur score, rappel d'inactivité).
|
||||
* Utilise un Handler pour planifier les tâches répétitives.
|
||||
* NOTE : Pour une robustesse accrue (garantie d'exécution même si l'app est tuée),
|
||||
* WorkManager serait préférable en production.
|
||||
*/
|
||||
public class NotificationService extends Service {
|
||||
|
||||
private static final int NOTIFICATION_ID_HIGHSCORE = 2; // Doit être différent des autres notifs
|
||||
private static final int NOTIFICATION_ID_INACTIVITY = 3;
|
||||
// Intervalles (exemples : 1 jour pour HS, 3 jours pour inactivité)
|
||||
private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
|
||||
private static final long INACTIVITY_INTERVAL_MS = TimeUnit.DAYS.toMillis(3);
|
||||
private static final long CHECK_INTERVAL_MS = TimeUnit.HOURS.toMillis(6); // Intervalle de vérification plus fréquent
|
||||
/** 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;
|
||||
|
||||
// Clés SharedPreferences (doivent correspondre à celles utilisées ailleurs)
|
||||
// --- 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";
|
||||
private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; // Nouvelle clé
|
||||
/** 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();
|
||||
// Utilise le Looper principal pour le Handler (simple, mais bloque si tâche longue)
|
||||
// Pour des tâches plus lourdes, utiliser HandlerThread
|
||||
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());
|
||||
// Pas besoin de créer le canal ici si MainActivity le fait déjà au démarrage
|
||||
// NotificationHelper.createNotificationChannel(this);
|
||||
|
||||
// Définit le Runnable qui sera exécuté périodiquement
|
||||
periodicTaskRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Vérifie périodiquement s'il faut envoyer une notification
|
||||
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 tâche
|
||||
handler.postDelayed(this, CHECK_INTERVAL_MS); // Vérifie toutes les X heures
|
||||
// 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).
|
||||
* <strong>NOTE: START_STICKY n'est pas une garantie d'exécution fiable pour les tâches
|
||||
* périodiques en arrière-plan. Utilisez WorkManager.</strong>
|
||||
*/
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// Lance la tâche périodique lors du démarrage du service
|
||||
handler.removeCallbacks(periodicTaskRunnable); // Assure qu'il n'y a pas de doublon
|
||||
handler.post(periodicTaskRunnable); // Lance immédiatement la première vérification
|
||||
Log.i(TAG, "onStartCommand: Service démarré (ou redémarré). StartId: " + startId);
|
||||
|
||||
// START_STICKY : Le système essaiera de recréer le service s'il est tué.
|
||||
// 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) {
|
||||
// Service non lié (Started Service)
|
||||
// Ce service n'est pas conçu pour être lié.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie les conditions et envoie les notifications périodiques si nécessaire.
|
||||
* 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);
|
||||
boolean notificationsEnabled = prefs.getBoolean("notifications_enabled", true); // Vérifie si activé
|
||||
|
||||
// 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) {
|
||||
// Si désactivé dans les prefs, on arrête potentiellement le service?
|
||||
// Ou juste ne rien envoyer. Pour l'instant, ne rien envoyer.
|
||||
// stopSelf(); // Arrêterait le service
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Notification High Score (Exemple: envoyer une fois par jour si non joué depuis ?) ---
|
||||
// Logique simplifiée: on envoie juste le rappel basé sur un flag ou temps (pas implémenté ici)
|
||||
// Pour une vraie app, il faudrait une logique pour ne pas spammer.
|
||||
// Exemple: Envoyer si le dernier envoi date de plus de HIGHSCORE_INTERVAL_MS ?
|
||||
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);
|
||||
// Temporairement on l'envoie à chaque check pour test (à modifier!)
|
||||
// if (shouldSendHighScoreNotification()) {
|
||||
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é.");
|
||||
}
|
||||
|
||||
|
||||
// --- Notification d'Inactivité ---
|
||||
// 3. Vérification pour la notification d'Inactivité
|
||||
long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0);
|
||||
if (lastPlayedTime > 0 && (System.currentTimeMillis() - lastPlayedTime > INACTIVITY_INTERVAL_MS)) {
|
||||
// Si l'inactivité dépasse le seuil
|
||||
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();
|
||||
// Optionnel: Mettre à jour lastPlayedTime pour ne pas renvoyer immédiatement ?
|
||||
// Ou attendre que l'utilisateur rejoue pour mettre à jour lastPlayedTime dans onPause.
|
||||
// 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.");
|
||||
}
|
||||
|
||||
/** Affiche la notification High Score */
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/** Affiche la notification d'Inactivité */
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
// Ajouter ici une logique plus fine si nécessaire pour savoir QUAND envoyer les notifs périodiques
|
||||
// private boolean shouldSendHighScoreNotification() { ... }
|
||||
|
||||
}
|
3570
sortie.txt
Normal file
3570
sortie.txt
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user