Refactor: Clean codebase, add MP features, improve notifications & robustness
This commit incorporates significant improvements and cleaning across the Best 2048 application. **Code Cleaning & Refactoring:** - Removed comments, logs (Log.*, System.out), and unused imports/variables/methods from all core Java files: - MainActivity, Game, GameStats, MultiplayerActivity, NotificationHelper, OnSwipeTouchListener, ApiClient, ApiService, data classes. - Removed NotificationService.java as it's replaced by WorkManager. **Notifications:** - Replaced the unreliable Handler-based NotificationService with a robust WorkManager implementation (NotificationWorker.java). - MainActivity now schedules/cancels periodic work for notifications correctly based on user preference and permissions. - Removed the <service> declaration for NotificationService from AndroidManifest.xml. - Requires 'androidx.work:work-runtime' dependency in build.gradle. **Multiplayer Enhancements:** - **Stats Integration:** - Added recordMultiplayerWin/Loss/Draw methods to GameStats. - MultiplayerActivity now correctly calculates game duration and updates GameStats upon game completion. - Added saveStats() call in MultiplayerActivity.onPause to persist MP stats. - **Animations:** - Implemented tile appearance and merge animations in MultiplayerActivity by comparing previous and current board states received via WebSocket. - **Robustness:** - Added automatic WebSocket reconnection attempts with UI feedback in MultiplayerActivity. - Implemented finer-grained handling of server error messages (critical vs. info). - Added UI feedback for opponent disconnections (inferred from final game state). - Disabled swipe input during inappropriate times (opponent's turn, disconnected, game over). **Layout Corrections:** - Fixed duplicate ID 'average_time_per_game_label' in stats_layout.xml (renamed the multiplayer one to 'average_time_per_game_multi_label'). - Removed the unused 'perfect_game_label' TextView from stats_layout.xml. - Updated MainActivity's updateStatisticsTextViews to use the corrected ID. **Localization:** - Translated all user-facing strings in strings.xml from French to English.
This commit is contained in:
parent
be983a1107
commit
1842213cac
@ -8,8 +8,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "legion.muyue.best2048"
|
||||
minSdk = 33
|
||||
targetSdk = 35
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@ -18,7 +18,8 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@ -46,4 +47,5 @@ dependencies {
|
||||
implementation(libs.logging.interceptor)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.work.runtime)
|
||||
}
|
@ -31,11 +31,6 @@
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Best2048" />
|
||||
|
||||
<service
|
||||
android:name=".NotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,42 +1,65 @@
|
||||
/**
|
||||
* Représente la logique métier du jeu 2048.
|
||||
* Gère l'état du plateau de jeu, les déplacements des tuiles, les fusions,
|
||||
* le score de la partie en cours, ainsi que les conditions de victoire ou de défaite.
|
||||
* Cette classe est conçue pour être indépendante du framework Android, ne contenant
|
||||
* aucune dépendance au Contexte Android ou aux SharedPreferences.
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable; // Ajout pour le retour de deserialize
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Représente la logique et l'état d'une partie du jeu 2048.
|
||||
* Gère le plateau de jeu, le score, la génération de nouvelles tuiles,
|
||||
* le traitement des mouvements du joueur, la détection des conditions de victoire et de fin de partie,
|
||||
* ainsi que la sérialisation et la désérialisation de base de l'état du jeu.
|
||||
* La taille du plateau est définie par {@link #BOARD_SIZE}.
|
||||
*/
|
||||
public class Game {
|
||||
|
||||
/** La taille du plateau de jeu (nombre de lignes/colonnes, typiquement 4x4). */
|
||||
/**
|
||||
* La taille (nombre de lignes et de colonnes) du plateau de jeu.
|
||||
* Fixé à 4 pour un jeu 2048 standard.
|
||||
*/
|
||||
private static final int BOARD_SIZE = 4;
|
||||
|
||||
/** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
|
||||
/**
|
||||
* Le plateau de jeu, représenté par une grille 2D d'entiers.
|
||||
* Chaque entier correspond à la valeur d'une tuile (0 pour une case vide).
|
||||
*/
|
||||
private int[][] board;
|
||||
/** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
|
||||
|
||||
/**
|
||||
* Générateur de nombres aléatoires utilisé pour placer de nouvelles tuiles
|
||||
* et déterminer leur valeur initiale (2, 4, 8, etc.).
|
||||
*/
|
||||
private final Random randomNumberGenerator;
|
||||
/** Score de la partie actuellement en cours. */
|
||||
|
||||
/**
|
||||
* Le score actuel de la partie en cours. Augmente lors de la fusion de tuiles.
|
||||
*/
|
||||
private int currentScore = 0;
|
||||
/** Meilleur score global connu par cette instance (généralement défini depuis l'extérieur). */
|
||||
|
||||
/**
|
||||
* Le meilleur score enregistré. Peut être chargé ou défini de l'extérieur.
|
||||
* Note : La persistance de ce score n'est pas gérée par cette classe.
|
||||
*/
|
||||
private int highestScore = 0;
|
||||
/** Indicateur si la condition de victoire (une tuile >= 2048) a été atteinte. */
|
||||
|
||||
/**
|
||||
* Indicateur de victoire. Passe à {@code true} lorsqu'une tuile >= 2048 est créée.
|
||||
* Le jeu peut continuer après la victoire.
|
||||
*/
|
||||
private boolean gameWon = false;
|
||||
/** Indicateur si la partie est terminée (plus de mouvements ou de fusions possibles). */
|
||||
|
||||
/**
|
||||
* Indicateur de fin de partie. Passe à {@code true} lorsqu'aucun mouvement valide n'est plus possible.
|
||||
*/
|
||||
private boolean gameOver = false;
|
||||
|
||||
/**
|
||||
* Constructeur pour démarrer une nouvelle partie.
|
||||
* Initialise un plateau vide, met le score courant à 0, réinitialise les états
|
||||
* de victoire/défaite, et ajoute deux tuiles initiales aléatoirement.
|
||||
* Constructeur par défaut pour démarrer une nouvelle partie.
|
||||
* Initialise un plateau vide, met le score à zéro, et ajoute deux tuiles initiales aléatoires.
|
||||
*/
|
||||
public Game() {
|
||||
this.randomNumberGenerator = new Random();
|
||||
@ -44,58 +67,54 @@ 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 et du score fournis.
|
||||
* Lance une exception si le plateau fourni n'a pas les bonnes dimensions.
|
||||
* Constructeur pour créer une instance de jeu à partir d'un état existant.
|
||||
* Utile pour charger une partie sauvegardée ou pour les tests.
|
||||
* Vérifie si les conditions de victoire ou de fin de partie sont déjà remplies.
|
||||
*
|
||||
* @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.
|
||||
* @param board Le plateau de jeu existant. Doit être non null et de taille {@code BOARD_SIZE}x{@code BOARD_SIZE}.
|
||||
* @param score Le score associé à l'état du plateau fourni.
|
||||
* @throws IllegalArgumentException si le plateau fourni est null ou n'a pas les dimensions correctes.
|
||||
*/
|
||||
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.
|
||||
// Crée une copie défensive du tableau pour éviter les modifications externes
|
||||
this.board = new int[BOARD_SIZE][BOARD_SIZE];
|
||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||
System.arraycopy(board[i], 0, this.board[i], 0, BOARD_SIZE);
|
||||
}
|
||||
this.currentScore = score;
|
||||
this.randomNumberGenerator = new Random();
|
||||
// Recalculer l'état de victoire/défaite basé sur le plateau chargé
|
||||
// Recalcule l'état win/over basé sur le plateau fourni
|
||||
checkWinCondition();
|
||||
// Vérifier si la partie chargée est déjà terminée
|
||||
checkGameOverCondition(); // Important si on charge une partie déjà terminée
|
||||
checkGameOverCondition();
|
||||
}
|
||||
|
||||
// --- Getters / Setters ---
|
||||
|
||||
/**
|
||||
* Retourne la valeur de la tuile aux coordonnées spécifiées (ligne, colonne).
|
||||
* Les indices sont basés sur 0.
|
||||
* Récupère la valeur de la tuile à la position spécifiée.
|
||||
*
|
||||
* @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.
|
||||
* @param row L'indice de la ligne (0 à BOARD_SIZE - 1).
|
||||
* @param column L'indice de la colonne (0 à BOARD_SIZE - 1).
|
||||
* @return La valeur de la tuile à la position (row, column), ou 0 si les indices sont invalides ou la case est vide.
|
||||
*/
|
||||
public int getCellValue(int row, int column) {
|
||||
if (isIndexValid(row, column)) {
|
||||
return this.board[row][column];
|
||||
}
|
||||
// Log ou gestion d'erreur pourrait être ajouté si un accès invalide est critique
|
||||
return 0; // Retourne 0 pour indice invalide comme convention
|
||||
return 0; // Retourne 0 si l'index est hors limites
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la valeur d'une tuile aux coordonnées spécifiées.
|
||||
* Utilisé principalement en interne ou pour les tests. Ne fait rien si
|
||||
* les coordonnées (ligne, colonne) sont en dehors des limites du plateau.
|
||||
* Définit la valeur d'une cellule spécifique sur le plateau.
|
||||
* Principalement destiné aux tests unitaires pour configurer des scénarios spécifiques.
|
||||
* Utiliser avec prudence car cela modifie directement l'état interne du jeu.
|
||||
*
|
||||
* @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.
|
||||
* @param row L'indice de la ligne (0 à BOARD_SIZE - 1).
|
||||
* @param col L'indice de la colonne (0 à BOARD_SIZE - 1).
|
||||
* @param value La nouvelle valeur pour la cellule.
|
||||
*/
|
||||
@VisibleForTesting // Indique que cette méthode est surtout pour les tests
|
||||
@VisibleForTesting
|
||||
void setCellValue(int row, int col, int value) {
|
||||
if (isIndexValid(row, col)) {
|
||||
this.board[row][col] = value;
|
||||
@ -103,135 +122,121 @@ public class Game {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le score actuel de la partie en cours.
|
||||
* Le score augmente lors de la fusion de tuiles.
|
||||
* Récupère le score actuel de la partie.
|
||||
*
|
||||
* @return Le score entier actuel.
|
||||
* @return Le score actuel.
|
||||
*/
|
||||
public int getCurrentScore() {
|
||||
return currentScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le meilleur score connu par cette instance de jeu.
|
||||
* Cette valeur est généralement chargée depuis une sauvegarde externe et mise à jour via {@link #setHighestScore(int)}.
|
||||
* Récupère le meilleur score enregistré (peut nécessiter une gestion externe).
|
||||
*
|
||||
* @return Le meilleur score entier connu.
|
||||
* @return Le meilleur score connu.
|
||||
*/
|
||||
public int getHighestScore() {
|
||||
return highestScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la valeur du meilleur score stockée dans cet objet Game.
|
||||
* Cette méthode est typiquement appelée par la classe gérant la persistance
|
||||
* (par exemple, MainActivity) après avoir chargé le meilleur score global.
|
||||
* Définit le meilleur score. Utile pour charger un meilleur score sauvegardé.
|
||||
*
|
||||
* @param highScore Le nouveau meilleur score global à stocker dans cette instance.
|
||||
* @param highScore Le meilleur score à définir.
|
||||
*/
|
||||
public void setHighestScore(int highScore) {
|
||||
this.highestScore = highScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la condition de victoire a été atteinte (au moins une tuile avec une valeur >= 2048).
|
||||
* Vérifie si la condition de victoire (atteindre une tuile >= 2048) a été remplie.
|
||||
*
|
||||
* @return true si le jeu est considéré comme gagné, false sinon.
|
||||
* @return {@code true} si le jeu est gagné, {@code 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.
|
||||
* Vérifie si la condition de fin de partie (aucun mouvement possible) a été remplie.
|
||||
*
|
||||
* @return true si la partie est terminée (game over), false sinon.
|
||||
* @return {@code true} si le jeu est terminé, {@code 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.
|
||||
* Définit l'état de victoire du jeu. Méthode privée utilisée en interne.
|
||||
* @param won Nouvel état de victoire.
|
||||
*/
|
||||
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.
|
||||
* Définit l'état de fin de partie du jeu. Méthode privée utilisée en interne.
|
||||
* @param over Nouvel état de fin de partie.
|
||||
*/
|
||||
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.
|
||||
* Retourne une copie du plateau de jeu actuel.
|
||||
* Utiliser cette méthode pour obtenir l'état du plateau sans risquer de
|
||||
* modifier l'état interne du jeu par inadvertance.
|
||||
*
|
||||
* @return Une nouvelle matrice 2D (`int[BOARD_SIZE][BOARD_SIZE]`) représentant l'état actuel du plateau.
|
||||
* @return Une copie 2D du tableau {@code board}. Ne sera jamais null.
|
||||
*/
|
||||
@NonNull
|
||||
public int[][] getBoard() {
|
||||
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
|
||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||
// System.arraycopy est efficace pour copier des tableaux primitifs
|
||||
// Utilise arraycopy pour une copie efficace de chaque ligne
|
||||
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Ré)Initialise le plateau de jeu pour une nouvelle partie.
|
||||
* Crée une nouvelle matrice vide (remplie de 0), réinitialise le score courant
|
||||
* et les indicateurs `gameWon`/`gameOver`, puis appelle {@link #addNewTile()}
|
||||
* deux fois pour placer les deux premières tuiles aléatoires.
|
||||
* Initialise ou réinitialise le plateau pour une nouvelle partie.
|
||||
* Met toutes les cellules à 0, réinitialise le score et les indicateurs
|
||||
* de victoire/fin de partie, puis ajoute deux nouvelles tuiles.
|
||||
*/
|
||||
private void initializeNewBoard() {
|
||||
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice remplie de zéros par défaut
|
||||
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée un nouveau tableau vide
|
||||
this.currentScore = 0;
|
||||
this.gameWon = false;
|
||||
this.gameOver = false;
|
||||
// Ajoute les deux premières tuiles requises pour démarrer une partie
|
||||
addNewTile();
|
||||
addNewTile();
|
||||
addNewTile(); // Ajoute la première tuile
|
||||
addNewTile(); // Ajoute la deuxième tuile
|
||||
}
|
||||
|
||||
// --- Logique du Jeu ---
|
||||
|
||||
/**
|
||||
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon les probabilités définies
|
||||
* dans {@link #generateRandomTileValue()}) sur une case vide choisie aléatoirement.
|
||||
* Si le plateau est plein (aucune case vide), cette méthode ne fait rien.
|
||||
* Ajoute une nouvelle tuile aléatoire (2, 4, 8, ...) sur une case vide aléatoire du plateau.
|
||||
* Si aucune case n'est vide, cette méthode n'a aucun effet.
|
||||
* La valeur de la nouvelle tuile est déterminée par {@link #generateRandomTileValue()}.
|
||||
*/
|
||||
public void addNewTile() {
|
||||
List<int[]> emptyCells = findEmptyCells();
|
||||
if (!emptyCells.isEmpty()) {
|
||||
// Choisit une cellule vide au hasard parmi celles disponibles
|
||||
// Choisit une cellule vide au hasard
|
||||
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
|
||||
// Génère la valeur de la nouvelle tuile (2, 4, 8, ...)
|
||||
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
|
||||
// Place la nouvelle tuile sur le plateau
|
||||
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.
|
||||
// Après ajout, revérifie si le jeu est terminé (au cas où l'ajout bloquerait tout)
|
||||
checkGameOverCondition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche et retourne les coordonnées de toutes les cellules vides (valeur 0) sur le plateau.
|
||||
* Trouve 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.
|
||||
* @return Une liste de tableaux d'entiers {@code [row, col]}, où chaque tableau représente
|
||||
* les coordonnées d'une cellule vide. Retourne une liste vide s'il n'y a pas de cellules vides. Ne sera jamais null.
|
||||
*/
|
||||
@NonNull
|
||||
private List<int[]> findEmptyCells() {
|
||||
@ -247,205 +252,193 @@ public class Game {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Génère la valeur pour une nouvelle tuile ajoutée au plateau.
|
||||
* Cette implémentation spécifique a des probabilités pour 2, 4, 8, 16, 32, 64, 128, 256.
|
||||
* ~85.4% de chance pour 2, ~12% pour 4, ~2% pour 8, ~0.5% pour 16, etc.
|
||||
*
|
||||
* 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.
|
||||
* @return La valeur de la nouvelle tuile (2, 4, 8, ... , 256).
|
||||
*/
|
||||
private int generateRandomTileValue() {
|
||||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour une granularité fine des pourcentages
|
||||
|
||||
// Les seuils définissent les probabilités cumulées
|
||||
if (randomValue < 8540) return 2; // 0 <= randomValue < 8540 (85.40%)
|
||||
if (randomValue < 9740) return 4; // 8540 <= randomValue < 9740 (12.00%)
|
||||
if (randomValue < 9940) return 8; // 9740 <= randomValue < 9940 (2.00%)
|
||||
if (randomValue < 9990) return 16; // 9940 <= randomValue < 9990 (0.50%)
|
||||
if (randomValue < 9995) return 32; // 9990 <= randomValue < 9995 (0.05%)
|
||||
if (randomValue < 9998) return 64; // 9995 <= randomValue < 9998 (0.03%)
|
||||
if (randomValue < 9999) return 128; // 9998 <= randomValue < 9999 (0.01%)
|
||||
return 256; // 9999 <= randomValue < 10000 (0.01%)
|
||||
// Note: Les probabilités ici diffèrent du 2048 standard (90% 2, 10% 4).
|
||||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour les pourcentages
|
||||
if (randomValue < 8540) return 2; // ~85.4%
|
||||
if (randomValue < 9740) return 4; // ~12.0% (9740 - 8540)
|
||||
if (randomValue < 9940) return 8; // ~ 2.0% (9940 - 9740)
|
||||
if (randomValue < 9990) return 16; // ~ 0.5% (9990 - 9940)
|
||||
if (randomValue < 9995) return 32; // ~0.05% (9995 - 9990)
|
||||
if (randomValue < 9998) return 64; // ~0.03% (9998 - 9995)
|
||||
if (randomValue < 9999) return 128;// ~0.01% (9999 - 9998)
|
||||
return 256; // ~0.01%
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers le HAUT.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
* Tente d'effectuer un mouvement vers le haut.
|
||||
* Déplace et fusionne les tuiles vers le haut.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
* @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
|
||||
*/
|
||||
public boolean pushUp() {
|
||||
return processMove(MoveDirection.UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers le BAS.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
* Tente d'effectuer un mouvement vers le bas.
|
||||
* Déplace et fusionne les tuiles vers le bas.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
* @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
|
||||
*/
|
||||
public boolean pushDown() {
|
||||
return processMove(MoveDirection.DOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
* Tente d'effectuer un mouvement vers la gauche.
|
||||
* Déplace et fusionne les tuiles vers la gauche.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
* @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
|
||||
*/
|
||||
public boolean pushLeft() {
|
||||
return processMove(MoveDirection.LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de déplacer et fusionner les tuiles vers la DROITE.
|
||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
||||
* Tente d'effectuer un mouvement vers la droite.
|
||||
* Déplace et fusionne les tuiles vers la droite.
|
||||
*
|
||||
* @return true si au moins une tuile a bougé ou fusionné, false si le plateau n'a pas changé.
|
||||
* @return {@code true} si le plateau a été modifié par le mouvement, {@code false} sinon.
|
||||
*/
|
||||
public boolean pushRight() {
|
||||
return processMove(MoveDirection.RIGHT);
|
||||
}
|
||||
|
||||
/** Énumération interne pour clarifier la direction du mouvement dans {@link #processMove}. */
|
||||
/**
|
||||
* Énumération interne représentant les quatre directions de mouvement possibles.
|
||||
*/
|
||||
private enum MoveDirection { UP, DOWN, LEFT, RIGHT }
|
||||
|
||||
/**
|
||||
* Méthode générique pour traiter un mouvement (déplacement et fusion) dans une direction donnée.
|
||||
* C'est le cœur de la logique de déplacement du jeu. Elle parcourt le plateau, déplace
|
||||
* les tuiles jusqu'au bout dans la direction demandée, puis gère les fusions éventuelles.
|
||||
* Traite un mouvement dans la direction spécifiée.
|
||||
* Parcourt le plateau ligne par ligne ou colonne par colonne, déplace les tuiles,
|
||||
* effectue les fusions, met à jour le score, et vérifie les conditions de victoire/fin de partie.
|
||||
*
|
||||
* @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.
|
||||
* @param direction La direction {@link MoveDirection} du mouvement à traiter.
|
||||
* @return {@code true} si au moins une tuile a bougé ou fusionné, {@code false} sinon.
|
||||
*/
|
||||
private boolean processMove(MoveDirection direction) {
|
||||
boolean boardChanged = false;
|
||||
boolean boardChanged = false; // Indicateur si le plateau a été modifié
|
||||
|
||||
// Itérer sur l'axe perpendiculaire au mouvement (colonnes pour UP/DOWN, lignes pour LEFT/RIGHT)
|
||||
// Itère sur les lignes (pour GAUCHE/DROITE) ou les colonnes (pour HAUT/BAS)
|
||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||
// Tableau pour suivre si une tuile sur la ligne/colonne cible a déjà résulté d'une fusion
|
||||
// pendant CE mouvement, pour éviter les fusions en chaîne (ex: 2-2-4 -> 4-4 -> 8 en un seul swipe)
|
||||
// Tableau pour suivre si une cellule de la ligne/colonne cible a déjà fusionné dans ce mouvement
|
||||
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.
|
||||
// Détermine l'ordre de parcours des cellules dans la ligne/colonne
|
||||
// Pour HAUT et GAUCHE : on part du début (index 1) vers la fin
|
||||
// Pour BAS et DROITE : on part de l'avant-dernière (index BOARD_SIZE - 2) vers le début
|
||||
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;
|
||||
|
||||
// Parcourt les cellules de la ligne/colonne
|
||||
for (int j = start; j != end; j += step) {
|
||||
int row, col;
|
||||
// Déterminer les coordonnées (row, col) de la tuile courante basée sur l'axe i et l'index j
|
||||
int row, col; // Coordonnées de la cellule actuelle (j)
|
||||
if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) {
|
||||
row = j; col = i; // Mouvement vertical, i est la colonne, j est la ligne
|
||||
// Pour HAUT/BAS, 'i' est la colonne, 'j' est la ligne
|
||||
row = j; col = i;
|
||||
} else {
|
||||
row = i; col = j; // Mouvement horizontal, i est la ligne, j est la colonne
|
||||
// Pour GAUCHE/DROITE, 'i' est la ligne, 'j' est la colonne
|
||||
row = i; col = j;
|
||||
}
|
||||
|
||||
int currentValue = getCellValue(row, col);
|
||||
int currentValue = getCellValue(row, col); // Valeur de la tuile actuelle
|
||||
|
||||
// Si la case n'est pas vide, on essaie de la déplacer/fusionner
|
||||
// Si la cellule n'est pas vide
|
||||
if (currentValue != 0) {
|
||||
int currentRow = row;
|
||||
int currentRow = row; // Position initiale de la tuile
|
||||
int currentCol = col;
|
||||
int targetRow = currentRow; // Position cible initiale
|
||||
int targetRow = currentRow; // Position cible après déplacement (sans fusion)
|
||||
int targetCol = currentCol;
|
||||
|
||||
// 1. Déplacement : Trouver la case la plus éloignée atteignable dans la direction du mouvement
|
||||
// Calcule la position de la case voisine 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"
|
||||
// Tant que la case voisine est valide et vide, on déplace la cible
|
||||
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
|
||||
// Calcule la case suivante pour continuer le déplacement
|
||||
nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||
nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
|
||||
}
|
||||
|
||||
// 2. Fusion : Vérifier la case immédiatement après la position cible (targetRow, targetCol)
|
||||
// La case à vérifier pour fusion est `nextRow`, `nextCol` (calculée juste avant ou après la sortie du while)
|
||||
int mergeTargetRow = nextRow;
|
||||
// nextRow/nextCol contient maintenant la première case non vide rencontrée ou une case invalide
|
||||
int mergeTargetRow = nextRow; // Case potentielle pour la fusion
|
||||
int mergeTargetCol = nextCol;
|
||||
// Index utilisé pour le tableau `hasMerged` (soit la ligne soit la colonne cible de la fusion)
|
||||
// Index utilisé pour vérifier si la case cible a déjà fusionné
|
||||
int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;
|
||||
|
||||
boolean merged = false;
|
||||
// Vérifier si la case de fusion potentielle est valide, a la même valeur, et n'a pas déjà fusionné
|
||||
if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
|
||||
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
|
||||
!hasMerged[mergeIndex]) // Crucial: empêche double fusion
|
||||
boolean merged = false; // Indicateur si une fusion a eu lieu pour cette tuile
|
||||
// Vérifie si une fusion est possible
|
||||
if (isIndexValid(mergeTargetRow, mergeTargetCol) && // La case cible est valide
|
||||
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue && // Elle a la même valeur
|
||||
!hasMerged[mergeIndex]) // Elle n'a pas déjà fusionné ce tour-ci
|
||||
{
|
||||
// Fusion !
|
||||
int newValue = currentValue * 2;
|
||||
setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible de la fusion
|
||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine de la tuile qui a bougé ET fusionné
|
||||
currentScore += newValue; // Ajoute la *nouvelle* valeur au score
|
||||
// --- Fusion ---
|
||||
int newValue = currentValue * 2; // Nouvelle valeur après fusion
|
||||
setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible
|
||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
||||
currentScore += newValue; // Augmente le score
|
||||
hasMerged[mergeIndex] = true; // Marque la case cible comme ayant fusionné
|
||||
boardChanged = true; // Le plateau a changé
|
||||
merged = true;
|
||||
merged = true; // La tuile a fusionné
|
||||
|
||||
// Vérifier immédiatement si cette fusion a créé une tuile >= 2048
|
||||
if (newValue >= 2048) {
|
||||
// Vérifie la condition de victoire après la fusion
|
||||
if (newValue >= 2048) { // Utiliser une constante WINNING_TILE serait mieux
|
||||
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
|
||||
// Si aucune fusion n'a eu lieu mais que la tuile doit bouger
|
||||
if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
|
||||
// --- Déplacement simple ---
|
||||
setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
|
||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
||||
boardChanged = true; // Le plateau a changé
|
||||
}
|
||||
} // Fin if (currentValue != 0)
|
||||
} // Fin boucle interne (j)
|
||||
} // Fin boucle externe (i)
|
||||
} // Fin boucle for interne (j)
|
||||
} // Fin boucle for 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
|
||||
// Après avoir traité toutes les lignes/colonnes, revérifie les conditions
|
||||
// (la condition de victoire peut avoir été atteinte pendant la fusion)
|
||||
checkWinCondition();
|
||||
// La condition de fin de partie doit être vérifiée après le mouvement
|
||||
checkGameOverCondition();
|
||||
|
||||
// Retourne si le plateau a été modifié
|
||||
return boardChanged;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Vérifie si les indices de ligne et colonne fournis sont valides pour le plateau de jeu actuel.
|
||||
* Vérifie si les coordonnées fournies (ligne, colonne) sont valides
|
||||
* par rapport à la taille du plateau ({@link #BOARD_SIZE}).
|
||||
*
|
||||
* @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.
|
||||
* @param row L'indice de la ligne à vérifier.
|
||||
* @param col L'indice de la colonne à vérifier.
|
||||
* @return {@code true} si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, {@code false} sinon.
|
||||
*/
|
||||
private boolean isIndexValid(int row, int col) {
|
||||
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sérialise l'état essentiel du jeu (plateau et score courant) en une chaîne de caractères.
|
||||
* Le format actuel est simple : toutes les valeurs du plateau séparées par des virgules,
|
||||
* suivies par le score courant, également séparé par une virgule.
|
||||
* Exemple pour un plateau 2x2 : "2,0,4,8,12" (où 12 est le score).
|
||||
* <p>
|
||||
* NOTE : Ce format est simple mais peut être fragile si la structure du jeu
|
||||
* ou les données à sauvegarder évoluent. Un format comme JSON pourrait offrir plus de flexibilité.
|
||||
* </p>
|
||||
* Fournit une représentation textuelle simple de l'état du jeu (plateau + score).
|
||||
* Le format est une chaîne de valeurs séparées par des virgules :
|
||||
* toutes les valeurs du plateau (ligne par ligne), suivies du score actuel.
|
||||
* Utilisé pour la sérialisation simple via {@link #deserialize(String)}.
|
||||
*
|
||||
* @return Une chaîne non nulle représentant l'état sérialisé du jeu.
|
||||
* @return Une chaîne représentant l'état du jeu. Ne sera jamais null.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
@ -456,149 +449,139 @@ public class Game {
|
||||
sb.append(board[row][col]).append(",");
|
||||
}
|
||||
}
|
||||
// Ajoute le score à la fin, séparé par une virgule
|
||||
// Ajoute le score à la fin, sans virgule après
|
||||
sb.append(currentScore);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée (désérialise) un objet {@code Game} à partir de sa représentation sous forme de chaîne,
|
||||
* telle que générée par {@link #toString()}.
|
||||
* Le meilleur score (`highestScore`) n'est pas inclus dans la chaîne et doit être
|
||||
* défini séparément sur l'objet retourné via {@link #setHighestScore(int)}.
|
||||
* <p>
|
||||
* NOTE : Cette méthode dépend du format spécifique généré par {@code toString()}.
|
||||
* </p>
|
||||
* Crée une instance de {@link Game} à partir d'une chaîne sérialisée.
|
||||
* La chaîne doit correspondre au format généré par {@link #toString()}.
|
||||
*
|
||||
* @param serializedState La chaîne représentant l'état du jeu (plateau + score courant).
|
||||
* @return Une nouvelle instance de {@code Game} si la désérialisation réussit,
|
||||
* ou {@code null} si la chaîne est invalide, vide, ou a un format incorrect
|
||||
* (mauvais nombre d'éléments, valeur non entière).
|
||||
* @param serializedState La chaîne contenant l'état du jeu sérialisé. Peut être null ou vide.
|
||||
* @return Une nouvelle instance de {@link Game} initialisée avec l'état désérialisé,
|
||||
* ou {@code null} si la chaîne est invalide, vide, null, ou ne correspond pas au format attendu.
|
||||
*/
|
||||
@Nullable
|
||||
public static Game deserialize(@Nullable String serializedState) {
|
||||
if (serializedState == null || serializedState.isEmpty()) {
|
||||
return null;
|
||||
return null; // Chaîne vide ou nulle
|
||||
}
|
||||
String[] values = serializedState.split(",");
|
||||
// Vérifie si le nombre d'éléments correspond à la taille du plateau + 1 (pour le score)
|
||||
// Vérifie si le nombre de valeurs correspond à (taille*taille + 1 score)
|
||||
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) {
|
||||
return null;
|
||||
return null; // Nombre incorrect de valeurs
|
||||
}
|
||||
|
||||
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
|
||||
int index = 0;
|
||||
try {
|
||||
// Remplit le plateau à partir des valeurs de la chaîne
|
||||
// Remplit le nouveau plateau
|
||||
for (int row = 0; row < BOARD_SIZE; row++) {
|
||||
for (int col = 0; col < BOARD_SIZE; col++) {
|
||||
newBoard[row][col] = Integer.parseInt(values[index++]);
|
||||
}
|
||||
}
|
||||
// Le dernier élément est le score
|
||||
int score = Integer.parseInt(values[index]);
|
||||
|
||||
// Crée une nouvelle instance de Game avec les données désérialisées
|
||||
// Le constructeur recalculera gameWon et gameOver.
|
||||
// Récupère le score
|
||||
int score = Integer.parseInt(values[index]); // La dernière valeur est le score
|
||||
// Crée et retourne le nouvel objet Game en utilisant le constructeur approprié
|
||||
return new Game(newBoard, score);
|
||||
|
||||
} catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
|
||||
// En cas d'erreur de format (valeur non entière, problème d'indice, ou dimension plateau incorrecte dans constructeur)
|
||||
// Log l'erreur pourrait être utile pour le débogage
|
||||
System.err.println("Erreur lors de la désérialisation de l'état du jeu: " + e.getMessage());
|
||||
return null;
|
||||
return null; // Échec de la désérialisation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la condition de victoire (au moins une tuile avec une valeur >= 2048)
|
||||
* est actuellement atteinte sur le plateau. Met à jour l'état interne `gameWon`.
|
||||
* Cette vérification s'arrête dès qu'une tuile gagnante est trouvée ou si le jeu
|
||||
* est déjà marqué comme gagné.
|
||||
* Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
|
||||
* Met à jour le flag {@link #gameWon} si nécessaire.
|
||||
* Ne fait rien si le jeu est déjà marqué comme gagné.
|
||||
*/
|
||||
private void checkWinCondition() {
|
||||
// Si le jeu est déjà marqué comme gagné, pas besoin de revérifier
|
||||
// Si déjà gagné, pas besoin de revérifier
|
||||
if (gameWon) {
|
||||
return;
|
||||
}
|
||||
// Parcours le plateau à la recherche d'une tuile >= 2048
|
||||
// Parcourt le plateau à la recherche d'une tuile >= 2048
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
if (getCellValue(r, c) >= 2048) {
|
||||
setGameWon(true); // Met à jour l'état interne
|
||||
return; // Sort dès qu'une condition de victoire est trouvée
|
||||
// Utiliser getCellValue pour la cohérence (bien que l'accès direct soit possible ici)
|
||||
if (getCellValue(r, c) >= 2048) { // Utiliser une constante WINNING_TILE = 2048
|
||||
setGameWon(true);
|
||||
// Pas besoin de continuer à chercher une fois la condition atteinte
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si la boucle se termine sans trouver de tuile >= 2048, gameWon reste false (ou son état précédent)
|
||||
// Si on arrive ici, aucune tuile >= 2048 n'a été trouvée
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la condition de fin de partie (Game Over) est atteinte.
|
||||
* Cela se produit si le plateau est plein (pas de case vide) ET si aucune
|
||||
* fusion n'est possible entre des tuiles adjacentes (horizontalement ou verticalement).
|
||||
* Met à jour l'état interne `gameOver`.
|
||||
* Vérifie si la condition de fin de partie est atteinte (plus aucun mouvement possible).
|
||||
* Un jeu est terminé s'il n'y a plus de cellules vides ET aucune paire de tuiles
|
||||
* adjacentes identiques (horizontalement ou verticalement).
|
||||
* Met à jour le flag {@link #gameOver} si nécessaire.
|
||||
* Ne fait rien si le jeu est déjà marqué comme terminé.
|
||||
*/
|
||||
private void checkGameOverCondition() {
|
||||
// Si le jeu est déjà terminé, pas besoin de revérifier
|
||||
// Si déjà terminé, pas besoin de revérifier
|
||||
if (gameOver) {
|
||||
return;
|
||||
}
|
||||
// Si il y a au moins une case vide, le jeu ne peut pas être terminé
|
||||
|
||||
// S'il y a au moins une cellule vide, le jeu n'est pas terminé
|
||||
if (hasEmptyCell()) {
|
||||
setGameOver(false);
|
||||
setGameOver(false); // Assure que le flag est bien à false
|
||||
return;
|
||||
}
|
||||
|
||||
// Le plateau est plein. Vérifier s'il existe au moins une fusion possible.
|
||||
// S'il n'y a pas de cellules vides, vérifie les fusions possibles
|
||||
// Parcourt toutes les cellules
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
int current = getCellValue(r, c);
|
||||
// Vérifie le voisin du HAUT (si existe)
|
||||
// Vérifie la fusion possible vers le haut (si pas sur la première ligne)
|
||||
if (r > 0 && getCellValue(r - 1, c) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers le haut
|
||||
setGameOver(false); return; // Mouvement possible trouvé
|
||||
}
|
||||
// Vérifie le voisin du BAS (si existe)
|
||||
// Vérifie la fusion possible vers le bas (si pas sur la dernière ligne)
|
||||
if (r < BOARD_SIZE - 1 && getCellValue(r + 1, c) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers le bas
|
||||
setGameOver(false); return; // Mouvement possible trouvé
|
||||
}
|
||||
// Vérifie le voisin de GAUCHE (si existe)
|
||||
// Vérifie la fusion possible vers la gauche (si pas sur la première colonne)
|
||||
if (c > 0 && getCellValue(r, c - 1) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers la gauche
|
||||
setGameOver(false); return; // Mouvement possible trouvé
|
||||
}
|
||||
// Vérifie le voisin de DROITE (si existe)
|
||||
// Vérifie la fusion possible vers la droite (si pas sur la dernière colonne)
|
||||
if (c < BOARD_SIZE - 1 && getCellValue(r, c + 1) == current) {
|
||||
setGameOver(false); return; // Fusion possible vers la droite
|
||||
setGameOver(false); return; // Mouvement possible trouvé
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si on arrive ici, le plateau est plein ET aucune fusion adjacente n'est possible.
|
||||
// Si on arrive ici, il n'y a pas de cellules vides ET aucune fusion possible
|
||||
setGameOver(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie rapidement si le plateau contient au moins une case vide (valeur 0).
|
||||
* Utilisé principalement par {@link #checkGameOverCondition()}.
|
||||
* Vérifie rapidement s'il existe au moins une cellule vide sur le plateau.
|
||||
*
|
||||
* @return true s'il existe au moins une case vide, false si le plateau est plein.
|
||||
* @return {@code true} s'il y a au moins une cellule avec la valeur 0, {@code false} sinon.
|
||||
*/
|
||||
private boolean hasEmptyCell() {
|
||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||
if (getCellValue(r, c) == 0) {
|
||||
return true; // Sort dès qu'une case vide est trouvée
|
||||
return true; // Trouvé une cellule vide
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // Aucune case vide trouvée après avoir parcouru tout le plateau
|
||||
return false; // Aucune cellule vide trouvée
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la valeur de la plus haute tuile (la plus grande valeur numérique)
|
||||
* actuellement présente sur le plateau de jeu.
|
||||
* Calcule et retourne la valeur de la plus haute tuile actuellement sur le plateau.
|
||||
*
|
||||
* @return La valeur maximale trouvée sur le plateau, ou 0 si le plateau est vide.
|
||||
* @return La valeur maximale parmi toutes les tuiles du plateau. Retourne 0 si le plateau est vide.
|
||||
*/
|
||||
public int getHighestTileValue() {
|
||||
int maxTile = 0;
|
||||
|
@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Gère la collecte, la persistance (via {@link SharedPreferences}) et l'accès
|
||||
* aux statistiques du jeu 2048.
|
||||
* Inclut des statistiques générales, solo, et des placeholders pour le multijoueur.
|
||||
* Charge et sauvegarde automatiquement les statistiques via SharedPreferences.
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@ -11,122 +5,130 @@ import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Gère le suivi, le stockage et la récupération des statistiques de jeu
|
||||
* pour les modes solo et multijoueur du jeu Best2048.
|
||||
* Utilise {@link SharedPreferences} pour la persistance des données.
|
||||
* Nécessite un {@link Context} Android pour l'initialisation.
|
||||
*
|
||||
* Cette classe suit diverses métriques telles que le nombre de parties jouées,
|
||||
* les scores, le temps de jeu, les mouvements, les fusions, les séries de victoires, etc.
|
||||
*/
|
||||
public class GameStats {
|
||||
|
||||
// --- Constantes pour SharedPreferences ---
|
||||
|
||||
/** Nom du fichier de préférences partagées utilisé pour stocker les statistiques et l'état du jeu. */
|
||||
/** Nom du fichier de préférences partagées utilisé pour stocker les statistiques. */
|
||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||
/**
|
||||
* Clé pour le meilleur score global.
|
||||
* NOTE : Cette clé est également utilisée directement par MainActivity/Game pour la sauvegarde/chargement
|
||||
* de l'état du jeu. La synchronisation est gérée entre les classes.
|
||||
*/
|
||||
/** Clé pour stocker le meilleur score global (principalement solo). */
|
||||
private static final String HIGH_SCORE_KEY = "high_score";
|
||||
|
||||
// Clés spécifiques aux statistiques persistantes
|
||||
// Clés pour les statistiques Solo
|
||||
/** Clé pour le nombre total de parties solo terminées (victoire ou défaite). */
|
||||
private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed";
|
||||
/** Clé pour le nombre total de parties solo démarrées. */
|
||||
private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted";
|
||||
/** Clé pour le nombre total de mouvements effectués en mode solo. */
|
||||
private static final String STATS_TOTAL_MOVES = "totalMoves";
|
||||
/** Clé pour le temps de jeu total cumulé en mode solo (en millisecondes). */
|
||||
private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs";
|
||||
/** Clé pour le nombre total de fusions de tuiles en mode solo. */
|
||||
private static final String STATS_TOTAL_MERGES = "totalMerges";
|
||||
/** Clé pour la valeur de la plus haute tuile jamais atteinte en mode solo. */
|
||||
private static final String STATS_HIGHEST_TILE = "highestTile";
|
||||
/** Clé pour le nombre de fois où l'objectif (ex: tuile 2048) a été atteint en mode solo. */
|
||||
private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached";
|
||||
// private static final String STATS_PERFECT_GAMES = "perfectGames"; // Clé supprimée car concept non défini
|
||||
/** Clé pour le meilleur temps pour atteindre l'objectif en mode solo (en millisecondes). */
|
||||
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
|
||||
/** Clé pour le pire (plus long) temps pour atteindre l'objectif en mode solo (en millisecondes). */
|
||||
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
|
||||
|
||||
// Clés pour les statistiques multijoueur (fonctionnalité future)
|
||||
// Clés pour les statistiques Multijoueur (persistées)
|
||||
/** Clé pour le nombre total de parties multijoueur gagnées. */
|
||||
private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon";
|
||||
/** Clé pour le nombre total de parties multijoueur jouées (terminées). */
|
||||
private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed";
|
||||
/** Clé pour la meilleure série de victoires consécutives en multijoueur. */
|
||||
private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak";
|
||||
/** Clé pour le score total cumulé en multijoueur. */
|
||||
private static final String STATS_MP_TOTAL_SCORE = "multiplayerTotalScore";
|
||||
/** Clé pour le temps de jeu total cumulé en multijoueur (en millisecondes). */
|
||||
private static final String STATS_MP_TOTAL_TIME_MS = "multiplayerTotalTimeMs";
|
||||
/** Clé pour le nombre total de parties multijoueur perdues. */
|
||||
private static final String STATS_MP_LOSSES = "totalMultiplayerLosses";
|
||||
/** Clé pour le meilleur score obtenu dans une seule partie multijoueur. */
|
||||
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore";
|
||||
|
||||
|
||||
// --- Champs de Statistiques ---
|
||||
|
||||
// Générales & Solo
|
||||
/** Nombre total de parties terminées (victoire ou défaite). */
|
||||
// --- Champs Solo & Général (persistés via SharedPreferences) ---
|
||||
/** Nombre total de parties solo terminées (victoires + défaites). Persisté. */
|
||||
private int totalGamesPlayed;
|
||||
/** Nombre total de parties démarrées (via bouton "Nouvelle Partie"). */
|
||||
/** Nombre total de parties solo commencées. Persisté. */
|
||||
private int totalGamesStarted;
|
||||
/** Nombre total de mouvements (swipes valides) effectués dans toutes les parties. */
|
||||
/** Nombre total de mouvements effectués dans toutes les parties solo. Persisté. */
|
||||
private int totalMoves;
|
||||
/** Temps total passé à jouer (en millisecondes) sur toutes les parties. */
|
||||
/** Temps de jeu total cumulé pour toutes les parties solo (en millisecondes). Persisté. */
|
||||
private long totalPlayTimeMs;
|
||||
/** Nombre total de fusions de tuiles effectuées dans toutes les parties. */
|
||||
/** Nombre total de fusions effectuées dans toutes les parties solo. Persisté. */
|
||||
private int totalMerges;
|
||||
/** Valeur de la plus haute tuile atteinte globalement dans toutes les parties. */
|
||||
/** Valeur de la plus haute tuile jamais atteinte en mode solo. Persisté. */
|
||||
private int highestTile;
|
||||
/** Nombre de fois où l'objectif (atteindre 2048 ou plus) a été atteint. */
|
||||
/** Nombre de fois où l'objectif (ex: 2048) a été atteint en solo. Persisté. */
|
||||
private int numberOfTimesObjectiveReached;
|
||||
// private int perfectGames; // Champ supprimé car concept non défini
|
||||
/** Meilleur temps (le plus court, en ms) pour atteindre l'objectif (>= 2048). */
|
||||
/** Meilleur temps (le plus court) pour atteindre l'objectif en solo (en millisecondes). Persisté. */
|
||||
private long bestWinningTimeMs;
|
||||
/** Pire temps (le plus long, en ms) pour atteindre l'objectif (>= 2048). */
|
||||
/** Pire temps (le plus long) pour atteindre l'objectif en solo (en millisecondes). Persisté. */
|
||||
private long worstWinningTimeMs;
|
||||
/**
|
||||
* Meilleur score global atteint. Persisté via HIGH_SCORE_KEY.
|
||||
* Synchronisé avec MainActivity/Game lors du chargement/sauvegarde.
|
||||
*/
|
||||
/** Meilleur score global atteint (principalement en mode solo). Persisté. */
|
||||
private int overallHighScore;
|
||||
|
||||
// Partie en cours (non persistées telles quelles, utilisées pour calculs en temps réel)
|
||||
/** Nombre de mouvements effectués dans la partie actuelle. */
|
||||
// --- Champs Partie en cours (Solo - transitoires, non persistés) ---
|
||||
/** Nombre de mouvements effectués dans la partie solo actuelle. Non persisté. */
|
||||
private int currentMoves;
|
||||
/** Timestamp (en ms) du début de la partie actuelle. Réinitialisé à chaque nouvelle partie ou reprise. */
|
||||
/** Timestamp (millisecondes) du début de la partie solo actuelle. Non persisté. */
|
||||
private long currentGameStartTimeMs;
|
||||
/** Nombre de fusions effectuées dans la partie actuelle. */
|
||||
/** Nombre de fusions effectuées dans la partie solo actuelle. Non persisté. */
|
||||
private int mergesThisGame;
|
||||
|
||||
// Multijoueur (placeholders pour fonctionnalité future)
|
||||
/** Nombre de parties multijoueur gagnées. */
|
||||
// --- Champs Multijoueur (persistés via SharedPreferences) ---
|
||||
/** Nombre total de parties multijoueur gagnées. Persisté. */
|
||||
private int multiplayerGamesWon;
|
||||
/** Nombre de parties multijoueur jouées (terminées). */
|
||||
/** Nombre total de parties multijoueur jouées (terminées). Persisté. */
|
||||
private int multiplayerGamesPlayed;
|
||||
/** Plus longue série de victoires consécutives en multijoueur. */
|
||||
/** Meilleure série de victoires consécutives en multijoueur jamais atteinte. Persisté. */
|
||||
private int multiplayerBestWinningStreak;
|
||||
/** Score total accumulé dans toutes les parties multijoueur. */
|
||||
/** Score total cumulé sur toutes les parties multijoueur. Persisté. */
|
||||
private long multiplayerTotalScore;
|
||||
/** Temps total passé dans les parties multijoueur (en ms). */
|
||||
/** Temps de jeu total cumulé pour toutes les parties multijoueur (en millisecondes). Persisté. */
|
||||
private long multiplayerTotalTimeMs;
|
||||
/** Nombre total de défaites en multijoueur. */
|
||||
/** Nombre total de parties multijoueur perdues. Persisté. */
|
||||
private int totalMultiplayerLosses;
|
||||
/** Meilleur score atteint dans une seule partie multijoueur. */
|
||||
/** Meilleur score obtenu dans une seule partie multijoueur. Persisté. */
|
||||
private int multiplayerHighestScore;
|
||||
|
||||
/** Contexte applicatif nécessaire pour accéder aux SharedPreferences. */
|
||||
// --- Champs Transitoires (non persistés) ---
|
||||
/** Série de victoires consécutives actuelle en multijoueur. Non persisté. */
|
||||
private int currentMultiplayerWinningStreak = 0;
|
||||
|
||||
/** Contexte Android nécessaire pour accéder aux SharedPreferences. */
|
||||
private final Context context;
|
||||
|
||||
// --- Constructeur & Persistance ---
|
||||
|
||||
/**
|
||||
* Constructeur de GameStats.
|
||||
* Initialise l'objet et charge immédiatement les statistiques persistantes
|
||||
* depuis les SharedPreferences via {@link #loadStats()}.
|
||||
* Construit une nouvelle instance de GameStats.
|
||||
* Charge immédiatement les statistiques persistées depuis les SharedPreferences.
|
||||
*
|
||||
* @param context Contexte de l'application (Activity ou Application). Utilise `getApplicationContext()` pour éviter les fuites de mémoire.
|
||||
* @param context Le contexte de l'application Android, nécessaire pour accéder aux SharedPreferences. Ne doit pas être null.
|
||||
*/
|
||||
public GameStats(Context context) {
|
||||
this.context = context.getApplicationContext(); // Important: utiliser le contexte applicatif
|
||||
loadStats(); // Charge les statistiques dès l'initialisation
|
||||
this.context = context.getApplicationContext(); // Utilise le contexte d'application pour éviter les fuites de mémoire
|
||||
loadStats(); // Charge les statistiques au moment de la création
|
||||
}
|
||||
|
||||
// --- Persistance (SharedPreferences) ---
|
||||
|
||||
/**
|
||||
* Charge toutes les statistiques persistantes depuis le fichier SharedPreferences défini par {@link #PREFS_NAME}.
|
||||
* Si une clé n'existe pas, une valeur par défaut est utilisée (0, Long.MAX_VALUE pour best time, etc.).
|
||||
* Appelé automatiquement par le constructeur.
|
||||
* Charge toutes les statistiques persistées depuis le fichier SharedPreferences défini par {@link #PREFS_NAME}.
|
||||
* Si une clé n'est pas trouvée, une valeur par défaut est utilisée (généralement 0 ou Long.MAX_VALUE pour best time).
|
||||
*/
|
||||
public void loadStats() {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
// Charge le high score (clé partagée)
|
||||
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||||
|
||||
// Charge les statistiques générales et solo
|
||||
totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0);
|
||||
totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0);
|
||||
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
|
||||
@ -134,11 +136,9 @@ public class GameStats {
|
||||
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
|
||||
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
|
||||
numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 0);
|
||||
// perfectGames = prefs.getInt(STATS_PERFECT_GAMES, 0); // Supprimé
|
||||
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme indicateur "pas encore de temps enregistré"
|
||||
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0L);
|
||||
|
||||
// Charge les statistiques multijoueur (placeholders)
|
||||
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE);
|
||||
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0L); // 0 est ok pour le pire temps
|
||||
// Chargement des stats Multi
|
||||
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
|
||||
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
|
||||
multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 0);
|
||||
@ -149,19 +149,14 @@ public class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde toutes les statistiques persistantes actuelles dans le fichier SharedPreferences.
|
||||
* Utilise `apply()` pour une sauvegarde asynchrone et non bloquante.
|
||||
* Devrait être appelée lorsque l'application se met en pause ou est détruite,
|
||||
* ou après une mise à jour significative des statistiques (ex: reset).
|
||||
* Sauvegarde l'état actuel de toutes les statistiques persistables dans le fichier SharedPreferences.
|
||||
* Utilise {@link SharedPreferences.Editor#apply()} pour une sauvegarde asynchrone en arrière-plan.
|
||||
* Les statistiques transitoires (partie en cours) ne sont pas sauvegardées.
|
||||
*/
|
||||
public void saveStats() {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
// Sauvegarde le high score (clé partagée)
|
||||
editor.putInt(HIGH_SCORE_KEY, overallHighScore);
|
||||
|
||||
// Sauvegarde les statistiques générales et solo
|
||||
editor.putInt(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed);
|
||||
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
|
||||
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
|
||||
@ -169,11 +164,9 @@ public class GameStats {
|
||||
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
|
||||
editor.putInt(STATS_HIGHEST_TILE, highestTile);
|
||||
editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached);
|
||||
// editor.putInt(STATS_PERFECT_GAMES, perfectGames); // Supprimé
|
||||
editor.putLong(STATS_BEST_WINNING_TIME_MS, bestWinningTimeMs);
|
||||
editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs);
|
||||
|
||||
// Sauvegarde les statistiques multijoueur (placeholders)
|
||||
// Sauvegarde des stats Multi
|
||||
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
|
||||
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
|
||||
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
|
||||
@ -181,29 +174,26 @@ public class GameStats {
|
||||
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
|
||||
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
|
||||
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
|
||||
|
||||
// Applique les changements de manière asynchrone
|
||||
editor.apply();
|
||||
editor.apply(); // Sauvegarde asynchrone
|
||||
}
|
||||
|
||||
// --- Méthodes de Mise à Jour des Statistiques ---
|
||||
// --- Méthodes de suivi pour le mode Solo ---
|
||||
|
||||
/**
|
||||
* Doit être appelée au début de chaque nouvelle partie (ex: clic sur "Nouvelle Partie").
|
||||
* Incrémente le compteur de parties démarrées (`totalGamesStarted`) et réinitialise
|
||||
* les statistiques spécifiques à la partie en cours (`currentMoves`, `mergesThisGame`, `currentGameStartTimeMs`).
|
||||
* Enregistre le début d'une nouvelle partie solo.
|
||||
* Incrémente le compteur de parties démarrées, réinitialise les compteurs de la partie actuelle
|
||||
* (mouvements, fusions) et enregistre l'heure de début.
|
||||
*/
|
||||
public void startGame() {
|
||||
totalGamesStarted++;
|
||||
currentMoves = 0;
|
||||
mergesThisGame = 0;
|
||||
currentGameStartTimeMs = System.currentTimeMillis(); // Enregistre le temps de début
|
||||
currentGameStartTimeMs = System.currentTimeMillis(); // Enregistre l'heure de début
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un mouvement réussi (un swipe qui a modifié l'état du plateau).
|
||||
* Incrémente le compteur de mouvements pour la partie en cours (`currentMoves`)
|
||||
* et le compteur total de mouvements (`totalMoves`).
|
||||
* Enregistre un mouvement effectué dans la partie solo actuelle.
|
||||
* Incrémente le compteur de mouvements pour la partie en cours et le compteur total de mouvements.
|
||||
*/
|
||||
public void recordMove() {
|
||||
currentMoves++;
|
||||
@ -211,11 +201,10 @@ public class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
|
||||
* Incrémente le compteur de fusions pour la partie en cours (`mergesThisGame`)
|
||||
* et le compteur total de fusions (`totalMerges`).
|
||||
* Enregistre une ou plusieurs fusions effectuées lors d'un mouvement en solo.
|
||||
* Incrémente le compteur de fusions pour la partie en cours et le compteur total de fusions.
|
||||
*
|
||||
* @param numberOfMerges Le nombre de fusions distinctes réalisées lors du dernier mouvement (typiquement 1 ou plus).
|
||||
* @param numberOfMerges Le nombre de fusions à ajouter (doit être > 0).
|
||||
*/
|
||||
public void recordMerge(int numberOfMerges) {
|
||||
if (numberOfMerges > 0) {
|
||||
@ -225,86 +214,144 @@ public class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la statistique de la plus haute tuile atteinte globalement (`highestTile`),
|
||||
* si la valeur fournie est supérieure à la valeur actuelle enregistrée.
|
||||
* Devrait être appelée après chaque mouvement qui pourrait créer une nouvelle tuile max.
|
||||
* Met à jour la valeur de la plus haute tuile jamais atteinte si la nouvelle valeur est supérieure.
|
||||
*
|
||||
* @param currentHighestTileValue La valeur de la tuile la plus haute sur le plateau à l'instant T.
|
||||
* @param tileValue La valeur de la tuile potentiellement la plus haute.
|
||||
*/
|
||||
public void updateHighestTile(int currentHighestTileValue) {
|
||||
if (currentHighestTileValue > this.highestTile) {
|
||||
this.highestTile = currentHighestTileValue;
|
||||
// La sauvegarde se fera via saveStats() globalement
|
||||
public void updateHighestTile(int tileValue) {
|
||||
if (tileValue > highestTile) {
|
||||
highestTile = tileValue;
|
||||
saveStats();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une victoire (atteinte de l'objectif >= 2048).
|
||||
* Incrémente le compteur de victoires (`numberOfTimesObjectiveReached`) et met à jour
|
||||
* les statistiques de meilleur/pire temps de victoire si nécessaire.
|
||||
* Appelle également {@link #endGame(long)} pour finaliser les statistiques générales de la partie.
|
||||
* Enregistre une victoire en mode solo (atteinte de l'objectif).
|
||||
* Incrémente le compteur de victoires, met à jour les meilleurs/pires temps de victoire si applicable,
|
||||
* et appelle {@link #endGame(long)} pour finaliser les statistiques de la partie.
|
||||
*
|
||||
* @param timeTakenMs Temps écoulé (en millisecondes) pour terminer cette partie gagnante.
|
||||
* @param timeMs Le temps pris pour gagner cette partie (en millisecondes).
|
||||
*/
|
||||
public void recordWin(long timeTakenMs) {
|
||||
public void recordWin(long timeMs) {
|
||||
numberOfTimesObjectiveReached++;
|
||||
if (timeTakenMs < bestWinningTimeMs) {
|
||||
bestWinningTimeMs = timeTakenMs;
|
||||
if (timeMs < bestWinningTimeMs) {
|
||||
bestWinningTimeMs = timeMs;
|
||||
}
|
||||
// Utiliser >= pour le pire temps permet de l'enregistrer même si c'est le premier
|
||||
if (timeTakenMs >= worstWinningTimeMs) {
|
||||
worstWinningTimeMs = timeTakenMs;
|
||||
// Utilise >= pour worstWinningTimeMs pour s'assurer qu'il est mis à jour même si le premier temps est 0
|
||||
if (timeMs >= worstWinningTimeMs) {
|
||||
worstWinningTimeMs = timeMs;
|
||||
}
|
||||
endGame(timeTakenMs); // Finalise aussi le temps total et le compteur de parties jouées
|
||||
endGame(timeMs); // Finalise la partie avec le temps enregistré
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une défaite (Game Over - plateau plein sans mouvement possible).
|
||||
* Calcule le temps écoulé pour la partie et appelle {@link #endGame(long)}
|
||||
* pour finaliser les statistiques générales.
|
||||
* Enregistre une défaite en mode solo.
|
||||
* Calcule le temps de jeu pour cette partie et appelle {@link #endGame(long)}.
|
||||
*/
|
||||
public void recordLoss() {
|
||||
// Calcule le temps écoulé pour cette partie avant de la finaliser
|
||||
long timeTakenMs = System.currentTimeMillis() - currentGameStartTimeMs;
|
||||
endGame(timeTakenMs);
|
||||
long timeMs = (currentGameStartTimeMs > 0) ? System.currentTimeMillis() - currentGameStartTimeMs : 0;
|
||||
endGame(timeMs); // Finalise la partie
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
|
||||
* Incrémente le compteur de parties jouées (`totalGamesPlayed`) et ajoute le temps
|
||||
* de la partie terminée au temps de jeu total (`totalPlayTimeMs`).
|
||||
* Méthode privée pour finaliser les statistiques communes à la fin d'une partie solo (victoire ou défaite).
|
||||
* Incrémente le compteur total de parties jouées et ajoute le temps de jeu de cette partie.
|
||||
*
|
||||
* @param timeTakenMs Temps total (en millisecondes) de la partie qui vient de se terminer.
|
||||
* @param timeMs Le temps de jeu pour la partie qui vient de se terminer (en millisecondes).
|
||||
*/
|
||||
private void endGame(long timeTakenMs) {
|
||||
private void endGame(long timeMs) {
|
||||
totalGamesPlayed++;
|
||||
addPlayTime(timeTakenMs); // Ajoute la durée de la partie terminée au total
|
||||
// Ne pas réinitialiser currentGameStartTimeMs ici, car une nouvelle partie
|
||||
// pourrait ne pas commencer immédiatement (ex: affichage dialogue Game Over).
|
||||
// startGame() s'en chargera.
|
||||
addPlayTime(timeMs);
|
||||
currentGameStartTimeMs = 0;
|
||||
saveStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une durée (en millisecondes) au temps de jeu total enregistré (`totalPlayTimeMs`).
|
||||
* Typiquement appelé dans `onPause` de l'activité pour enregistrer le temps écoulé
|
||||
* depuis `onResume` ou `startGame`.
|
||||
* Ajoute une durée au temps de jeu total cumulé en mode solo.
|
||||
*
|
||||
* @param durationMs Durée positive (en millisecondes) à ajouter au temps total.
|
||||
* @param durationMs La durée à ajouter (en millisecondes, doit être > 0).
|
||||
*/
|
||||
public void addPlayTime(long durationMs) {
|
||||
if (durationMs > 0) {
|
||||
this.totalPlayTimeMs += durationMs;
|
||||
totalPlayTimeMs += durationMs;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Méthodes de suivi pour le mode Multijoueur ---
|
||||
|
||||
/**
|
||||
* Réinitialise toutes les statistiques (générales, solo, multijoueur, y compris le high score global)
|
||||
* à leurs valeurs par défaut (0 ou équivalent).
|
||||
* Après la réinitialisation, les nouvelles valeurs sont immédiatement sauvegardées
|
||||
* dans les SharedPreferences via {@link #saveStats()}.
|
||||
* Méthode privée pour mettre à jour les statistiques communes à la fin d'une partie multijoueur.
|
||||
* Met à jour le nombre de parties jouées, le score total, le temps total et le meilleur score multi.
|
||||
*
|
||||
* @param score Le score obtenu dans la partie multijoueur terminée.
|
||||
* @param durationMs La durée de la partie multijoueur (en millisecondes).
|
||||
*/
|
||||
private void endMultiplayerGame(int score, long durationMs) {
|
||||
multiplayerGamesPlayed++;
|
||||
multiplayerTotalScore += score;
|
||||
if (durationMs > 0) {
|
||||
multiplayerTotalTimeMs += durationMs;
|
||||
}
|
||||
if (score > multiplayerHighestScore) {
|
||||
multiplayerHighestScore = score;
|
||||
}
|
||||
// Sauvegarde souvent après une partie multi pour ne pas perdre le résultat
|
||||
saveStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une victoire en multijoueur.
|
||||
* Incrémente le compteur de victoires multi, met à jour la série de victoires (actuelle et meilleure),
|
||||
* et appelle {@link #endMultiplayerGame(int, long)}.
|
||||
*
|
||||
* @param score Le score obtenu dans la partie gagnée.
|
||||
* @param durationMs La durée de la partie gagnée (en millisecondes).
|
||||
*/
|
||||
public void recordMultiplayerWin(int score, long durationMs) {
|
||||
multiplayerGamesWon++;
|
||||
currentMultiplayerWinningStreak++; // Incrémente la série actuelle
|
||||
if (currentMultiplayerWinningStreak > multiplayerBestWinningStreak) {
|
||||
multiplayerBestWinningStreak = currentMultiplayerWinningStreak; // Met à jour la meilleure série si nécessaire
|
||||
}
|
||||
endMultiplayerGame(score, durationMs); // Finalise les stats communes
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une défaite en multijoueur.
|
||||
* Incrémente le compteur de défaites multi, réinitialise la série de victoires actuelle,
|
||||
* et appelle {@link #endMultiplayerGame(int, long)}.
|
||||
*
|
||||
* @param score Le score obtenu dans la partie perdue.
|
||||
* @param durationMs La durée de la partie perdue (en millisecondes).
|
||||
*/
|
||||
public void recordMultiplayerLoss(int score, long durationMs) {
|
||||
totalMultiplayerLosses++;
|
||||
currentMultiplayerWinningStreak = 0; // Réinitialise la série de victoires
|
||||
endMultiplayerGame(score, durationMs); // Finalise les stats communes
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un match nul en multijoueur.
|
||||
* Réinitialise la série de victoires actuelle et appelle {@link #endMultiplayerGame(int, long)}.
|
||||
* Note: Un match nul compte comme une partie jouée mais n'affecte pas les victoires/défaites.
|
||||
*
|
||||
* @param score Le score obtenu dans le match nul.
|
||||
* @param durationMs La durée du match nul (en millisecondes).
|
||||
*/
|
||||
public void recordMultiplayerDraw(int score, long durationMs) {
|
||||
currentMultiplayerWinningStreak = 0; // Réinitialise la série de victoires (convention)
|
||||
endMultiplayerGame(score, durationMs); // Finalise les stats communes
|
||||
}
|
||||
|
||||
// --- Réinitialisation ---
|
||||
|
||||
/**
|
||||
* Réinitialise toutes les statistiques (solo et multijoueur, persistées et transitoires)
|
||||
* à leurs valeurs par défaut.
|
||||
* Sauvegarde immédiatement l'état réinitialisé dans les SharedPreferences.
|
||||
*/
|
||||
public void resetStats() {
|
||||
// Réinitialise toutes les variables membres à 0 ou valeur initiale
|
||||
// Réinitialisation Solo & Général
|
||||
overallHighScore = 0;
|
||||
totalGamesPlayed = 0;
|
||||
totalGamesStarted = 0;
|
||||
@ -313,11 +360,9 @@ public class GameStats {
|
||||
totalMerges = 0;
|
||||
highestTile = 0;
|
||||
numberOfTimesObjectiveReached = 0;
|
||||
// perfectGames = 0; // Supprimé
|
||||
bestWinningTimeMs = Long.MAX_VALUE;
|
||||
worstWinningTimeMs = 0L;
|
||||
|
||||
// Réinitialise les stats multijoueur (placeholders)
|
||||
// Réinitialisation Multi
|
||||
multiplayerGamesWon = 0;
|
||||
multiplayerGamesPlayed = 0;
|
||||
multiplayerBestWinningStreak = 0;
|
||||
@ -325,96 +370,94 @@ public class GameStats {
|
||||
multiplayerTotalTimeMs = 0L;
|
||||
totalMultiplayerLosses = 0;
|
||||
multiplayerHighestScore = 0;
|
||||
|
||||
// Réinitialise les stats de la partie en cours (au cas où)
|
||||
// Réinitialisation Transitoire
|
||||
currentMoves = 0;
|
||||
mergesThisGame = 0;
|
||||
currentGameStartTimeMs = 0L; // Ou System.currentTimeMillis() si on considère que reset démarre une nouvelle session? Non, 0 est plus sûr.
|
||||
currentGameStartTimeMs = 0L;
|
||||
currentMultiplayerWinningStreak = 0;
|
||||
|
||||
// Sauvegarde immédiatement les statistiques réinitialisées
|
||||
saveStats();
|
||||
saveStats(); // Sauvegarde l'état réinitialisé
|
||||
}
|
||||
|
||||
// --- Getters pour l'affichage ---
|
||||
// --- Getters (Accesseurs) ---
|
||||
|
||||
/** @return Le nombre total de parties terminées (victoire ou défaite). */
|
||||
/** @return Le nombre total de parties solo terminées. */
|
||||
public int getTotalGamesPlayed() { return totalGamesPlayed; }
|
||||
/** @return Le nombre total de parties démarrées. */
|
||||
/** @return Le nombre total de parties solo démarrées. */
|
||||
public int getTotalGamesStarted() { return totalGamesStarted; }
|
||||
/** @return Le nombre total de mouvements valides effectués. */
|
||||
/** @return Le nombre total de mouvements effectués en solo. */
|
||||
public int getTotalMoves() { return totalMoves; }
|
||||
/** @return Le nombre de mouvements dans la partie en cours. */
|
||||
/** @return Le nombre de mouvements dans la partie solo actuelle. */
|
||||
public int getCurrentMoves() { return currentMoves; }
|
||||
/** @return Le temps total de jeu accumulé (en ms). */
|
||||
/** @return Le temps de jeu total cumulé en solo (millisecondes). */
|
||||
public long getTotalPlayTimeMs() { return totalPlayTimeMs; }
|
||||
/** @return Le timestamp (en ms) du début de la partie en cours. Utile pour calculer la durée en temps réel. */
|
||||
/** @return Le timestamp de début de la partie solo actuelle (millisecondes). */
|
||||
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; }
|
||||
/** @return Le nombre de fusions dans la partie en cours. */
|
||||
/** @return Le nombre de fusions dans la partie solo actuelle. */
|
||||
public int getMergesThisGame() { return mergesThisGame; }
|
||||
/** @return Le nombre total de fusions effectuées. */
|
||||
/** @return Le nombre total de fusions effectuées en solo. */
|
||||
public int getTotalMerges() { return totalMerges; }
|
||||
/** @return La valeur de la plus haute tuile atteinte globalement. */
|
||||
/** @return La valeur de la plus haute tuile jamais atteinte en solo. */
|
||||
public int getHighestTile() { return highestTile; }
|
||||
/** @return Le nombre de fois où l'objectif (>= 2048) a été atteint. */
|
||||
/** @return Le nombre de fois où l'objectif a été atteint en solo. */
|
||||
public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; }
|
||||
// public int getPerfectGames() { return perfectGames; } // Supprimé
|
||||
/** @return Le meilleur temps (le plus court, en ms) pour gagner une partie, ou Long.MAX_VALUE si aucune victoire. */
|
||||
/** @return Le meilleur temps pour atteindre l'objectif en solo (millisecondes), ou Long.MAX_VALUE si jamais atteint. */
|
||||
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
|
||||
/** @return Le pire temps (le plus long, en ms) pour gagner une partie, ou 0 si aucune victoire. */
|
||||
/** @return Le pire temps pour atteindre l'objectif en solo (millisecondes). */
|
||||
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
|
||||
/** @return Le meilleur score global enregistré. */
|
||||
/** @return Le meilleur score global atteint (principalement solo). */
|
||||
public int getOverallHighScore() { return overallHighScore; }
|
||||
|
||||
// Getters Multiplayer (fonctionnalité future)
|
||||
/** @return Nombre de parties multijoueur gagnées. */
|
||||
// --- Getters Multijoueur ---
|
||||
/** @return Le nombre total de parties multijoueur gagnées. */
|
||||
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
|
||||
/** @return Nombre de parties multijoueur jouées. */
|
||||
/** @return Le nombre total de parties multijoueur jouées. */
|
||||
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
|
||||
/** @return Meilleure série de victoires consécutives en multijoueur. */
|
||||
/** @return La meilleure série de victoires consécutives en multijoueur. */
|
||||
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
|
||||
/** @return Score total accumulé en multijoueur. */
|
||||
/** @return Le score total cumulé en multijoueur. */
|
||||
public long getMultiplayerTotalScore() { return multiplayerTotalScore; }
|
||||
/** @return Temps total passé en multijoueur (en ms). */
|
||||
/** @return Le temps de jeu total cumulé en multijoueur (millisecondes). */
|
||||
public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; }
|
||||
/** @return Nombre total de défaites en multijoueur. */
|
||||
/** @return Le nombre total de parties multijoueur perdues. */
|
||||
public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; }
|
||||
/** @return Meilleur score atteint dans une partie multijoueur. */
|
||||
/** @return Le meilleur score obtenu dans une seule partie multijoueur. */
|
||||
public int getMultiplayerHighestScore() { return multiplayerHighestScore; }
|
||||
/** @return La série de victoires consécutives actuelle en multijoueur (non persisté). */
|
||||
public int getCurrentMultiplayerWinningStreak() { return currentMultiplayerWinningStreak; }
|
||||
|
||||
// --- Setters ---
|
||||
|
||||
// --- Setters (Mutateurs) ---
|
||||
|
||||
/**
|
||||
* Met à jour la valeur interne du meilleur score global (`overallHighScore`).
|
||||
* La mise à jour n'a lieu que si le score fourni est strictement supérieur
|
||||
* au meilleur score actuel. La sauvegarde effective dans SharedPreferences
|
||||
* se fait via un appel ultérieur à {@link #saveStats()}.
|
||||
* Met à jour le meilleur score global si le score fourni est supérieur.
|
||||
* Utile pour enregistrer un nouveau record après une partie solo.
|
||||
* Ne sauvegarde pas automatiquement, appeler {@link #saveStats()} si nécessaire.
|
||||
*
|
||||
* @param highScore Le nouveau score à potentiellement définir comme meilleur score.
|
||||
* @param score Le nouveau score potentiellement plus élevé.
|
||||
*/
|
||||
public void setHighestScore(int highScore) {
|
||||
// Met à jour seulement si la nouvelle valeur est meilleure
|
||||
if (highScore > this.overallHighScore) {
|
||||
this.overallHighScore = highScore;
|
||||
// La sauvegarde se fait via saveStats() globalement, pas ici directement.
|
||||
public void setHighestScore(int score) {
|
||||
if (score > overallHighScore) {
|
||||
overallHighScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le timestamp (en ms) du début de la partie en cours.
|
||||
* Typiquement utilisé par MainActivity dans `onResume` pour reprendre le chronomètre.
|
||||
* Définit manuellement l'heure de début de la partie en cours.
|
||||
* Peut être utile si l'état du jeu est restauré d'une source externe.
|
||||
*
|
||||
* @param timeMs Le timestamp en millisecondes.
|
||||
* @param timeMs Le timestamp de début en millisecondes.
|
||||
*/
|
||||
public void setCurrentGameStartTimeMs(long timeMs) {
|
||||
this.currentGameStartTimeMs = timeMs;
|
||||
currentGameStartTimeMs = timeMs;
|
||||
}
|
||||
|
||||
// --- Méthodes Calculées ---
|
||||
|
||||
/**
|
||||
* Calcule le temps moyen passé par partie terminée (solo).
|
||||
* Calcule le temps de jeu moyen par partie solo terminée.
|
||||
*
|
||||
* @return Le temps moyen par partie en millisecondes, ou 0 si aucune partie n'a été terminée.
|
||||
* @return Le temps moyen en millisecondes, ou 0 si aucune partie n'a été jouée.
|
||||
*/
|
||||
public long getAverageGameTimeMs() {
|
||||
return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0L;
|
||||
@ -422,44 +465,44 @@ public class GameStats {
|
||||
|
||||
/**
|
||||
* Calcule le score moyen par partie multijoueur terminée.
|
||||
* (Basé sur les statistiques multijoueur placeholder).
|
||||
*
|
||||
* @return Le score moyen entier par partie multijoueur, ou 0 si aucune partie multijoueur n'a été jouée.
|
||||
* @return Le score moyen, ou 0 si aucune partie multijoueur n'a été jouée.
|
||||
*/
|
||||
public int getMultiplayerAverageScore() {
|
||||
return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le temps moyen passé par partie multijoueur terminée.
|
||||
* (Basé sur les statistiques multijoueur placeholder).
|
||||
* Calcule le temps de jeu moyen par partie multijoueur terminée.
|
||||
*
|
||||
* @return Le temps moyen par partie multijoueur en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée.
|
||||
* @return Le temps moyen en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée.
|
||||
*/
|
||||
public long getMultiplayerAverageTimeMs() {
|
||||
return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une durée donnée en millisecondes en une chaîne de caractères lisible.
|
||||
* Le format est "hh:mm:ss" si la durée est d'une heure ou plus, sinon "mm:ss".
|
||||
* Formate une durée en millisecondes en une chaîne lisible "HH:MM:SS" ou "MM:SS".
|
||||
* Gère les durées négatives en les traitant comme 0.
|
||||
* Supprime le lint warning pour DefaultLocale car le format numérique est indépendant de la locale.
|
||||
*
|
||||
* @param milliseconds Durée totale en millisecondes.
|
||||
* @param ms La durée en millisecondes à formater.
|
||||
* @return Une chaîne formatée représentant la durée.
|
||||
*/
|
||||
@SuppressLint("DefaultLocale") // Justifié car le format hh:mm:ss est standard et non dépendant de la locale
|
||||
public static String formatTime(long milliseconds) {
|
||||
if (milliseconds < 0) { milliseconds = 0; } // Gère les cas négatifs
|
||||
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; // Minutes restantes après les heures
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60; // Secondes restantes après les minutes
|
||||
@SuppressLint("DefaultLocale") // Le format %02d est indépendant de la locale
|
||||
public static String formatTime(long ms) {
|
||||
if (ms < 0) {
|
||||
ms = 0; // Traite les durées négatives comme 0
|
||||
}
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(ms);
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(ms) % 60; // Minutes restantes après les heures
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(ms) % 60; // Secondes restantes après les minutes
|
||||
|
||||
if (hours > 0) {
|
||||
// Format avec heures si nécessaire
|
||||
// Format avec heures si la durée est >= 1 heure
|
||||
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
} else {
|
||||
// Format minutes:secondes sinon
|
||||
// Format sans heures si la durée est < 1 heure
|
||||
return String.format("%02d:%02d", minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,3 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -11,143 +5,128 @@ 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.content.pm.PackageManager;
|
||||
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.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
// import androidx.core.content.ContextCompat; // Peut être utilisé mais ActivityCompat est plus direct pour la permission ici
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour aider à la création et à l'affichage des notifications Android
|
||||
* pour l'application Best2048.
|
||||
* Gère la création du canal de notification requis à partir d'Android 8 (Oreo)
|
||||
* et fournit une méthode standardisée pour afficher les notifications,
|
||||
* en incluant la vérification des permissions pour Android 13+ et la configuration
|
||||
* d'un {@link PendingIntent} pour ouvrir l'application.
|
||||
*
|
||||
* <p>Il est recommandé d'appeler {@link #createNotificationChannel(Context)} une fois
|
||||
* au démarrage de l'application (par exemple, dans la méthode {@code onCreate} de la classe Application)
|
||||
* pour s'assurer que le canal est disponible avant d'envoyer des notifications sur les appareils
|
||||
* Android 8.0+.</p>
|
||||
*/
|
||||
public class NotificationHelper {
|
||||
|
||||
/**
|
||||
* Identifiant unique et constant pour le canal de notification de cette application.
|
||||
* Ce même ID doit être utilisé lors de la création de la notification via {@link NotificationCompat.Builder}.
|
||||
* Il est visible par l'utilisateur dans les paramètres de notification de l'application sur Android 8.0+.
|
||||
* Identifiant unique et constant pour le canal de notification de l'application.
|
||||
* Requis pour Android 8.0 (API niveau 26) et supérieur.
|
||||
*/
|
||||
public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé dans MainActivity/Service
|
||||
|
||||
/** Tag pour les logs spécifiques à ce helper. */
|
||||
private static final String TAG = "NotificationHelper";
|
||||
public static final String CHANNEL_ID = "BEST_2048_CHANNEL";
|
||||
|
||||
/**
|
||||
* Crée le canal de notification requis pour afficher des notifications sur Android 8.0 (API 26) et supérieur.
|
||||
* Si le canal existe déjà, cette méthode ne fait rien (elle est idempotente).
|
||||
* Doit être appelée une fois, idéalement au démarrage de l'application (par exemple dans `Application.onCreate`
|
||||
* ou dans `MainActivity.onCreate`), avant d'afficher la toute première notification sur un appareil API 26+.
|
||||
* Constructeur privé pour empêcher l'instanciation de cette classe utilitaire.
|
||||
*/
|
||||
private NotificationHelper() {
|
||||
// Classe utilitaire, ne doit pas être instanciée.
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le canal de notification nécessaire pour afficher des notifications sur
|
||||
* Android 8.0 (Oreo, API niveau 26) et supérieur.
|
||||
* Si l'application cible une version d'Android inférieure ou si le canal existe déjà,
|
||||
* cette méthode n'a aucun effet ou n'est pas nécessaire.
|
||||
* Doit être appelée avant de tenter d'afficher une notification sur les appareils concernés.
|
||||
* Utilise des ressources string ({@code R.string}) pour le nom et la description du canal.
|
||||
*
|
||||
* @param context Le contexte applicatif ({@code Context}) utilisé pour accéder aux services système.
|
||||
* @param context Le contexte de l'application, nécessaire pour accéder aux services système
|
||||
* et aux ressources. Ne doit pas être null.
|
||||
*/
|
||||
public static void createNotificationChannel(@NonNull Context context) {
|
||||
// La création de canal n'est nécessaire que sur Android Oreo (API 26) et versions ultérieures
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Récupère les chaînes de caractères pour le nom et la description du canal depuis les ressources
|
||||
CharSequence name = context.getString(R.string.notification_channel_name);
|
||||
String description = context.getString(R.string.notification_channel_description);
|
||||
// Définit l'importance du canal (affecte comment la notification est présentée à l'utilisateur)
|
||||
// IMPORTANCE_DEFAULT est un bon point de départ pour la plupart des notifications.
|
||||
int importance = NotificationManager.IMPORTANCE_DEFAULT;
|
||||
// Définit l'importance du canal (affecte comment la notification est présentée)
|
||||
int importance = NotificationManager.IMPORTANCE_DEFAULT; // Ou IMPORTANCE_HIGH, etc.
|
||||
|
||||
// Crée l'objet NotificationChannel
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
// Optionnel : Configurer d'autres propriétés du canal ici (vibration, lumière, son par défaut, etc.)
|
||||
// channel.enableLights(true);
|
||||
// channel.setLightColor(Color.RED);
|
||||
// channel.enableVibration(true);
|
||||
// channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
|
||||
|
||||
// Récupère le NotificationManager système
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
|
||||
// Enregistre le canal auprès du système. Si le canal existe déjà, aucune opération n'est effectuée.
|
||||
if (notificationManager != null) {
|
||||
Log.d(TAG, "Création ou mise à jour du canal de notification: " + CHANNEL_ID);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
} else {
|
||||
Log.e(TAG, "NotificationManager non disponible, impossible de créer le canal.");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "API < 26, pas besoin de créer de canal de notification.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit et affiche une notification standard pour l'application.
|
||||
* Vérifie la permission {@code POST_NOTIFICATIONS} sur Android 13 (API 33) et supérieur
|
||||
* avant de tenter d'afficher la notification. Si la permission est manquante,
|
||||
* un message d'erreur est logué et la méthode se termine sans afficher de notification.
|
||||
* Gère la vérification de la permission {@code POST_NOTIFICATIONS} pour Android 13+ (Tiramisu).
|
||||
* Configure un {@link PendingIntent} pour ouvrir {@link MainActivity} lorsque l'utilisateur
|
||||
* appuie sur la notification.
|
||||
* Utilise {@link NotificationCompat} pour la compatibilité avec les anciennes versions d'Android.
|
||||
*
|
||||
* @param context Le contexte ({@code Context}, peut être une Activity, un Service, etc.)
|
||||
* utilisé pour construire la notification et accéder aux ressources.
|
||||
* @param title Le titre à afficher dans la notification.
|
||||
* @param message Le contenu textuel principal de la notification.
|
||||
* @param notificationId Un identifiant entier unique pour cette notification. Permet de mettre à jour
|
||||
* ou d'annuler cette notification spécifique plus tard en utilisant le même ID.
|
||||
* @param context Le contexte de l'application. Ne doit pas être null.
|
||||
* @param title Le titre de la notification. Ne doit pas être null.
|
||||
* @param message Le message principal (corps) de la notification. Ne doit pas être null.
|
||||
* @param notificationId Un identifiant entier unique pour cette notification. Si une notification
|
||||
* avec le même ID existe déjà, elle sera mise à jour.
|
||||
*/
|
||||
public static void showNotification(@NonNull Context context, @NonNull String title, @NonNull String message, int notificationId) {
|
||||
Log.d(TAG, "Tentative d'affichage de la notification ID: " + notificationId + " Titre: " + title);
|
||||
|
||||
// --- Vérification de la permission pour Android 13 (API 33) et supérieur ---
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// Utilise ActivityCompat.checkSelfPermission pour vérifier la permission
|
||||
// Vérifie si la permission d'envoyer des notifications est accordée
|
||||
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// Si la permission n'est pas accordée, loguer un avertissement et ne pas continuer.
|
||||
// L'application appelante (ex: MainActivity, Service) est responsable de demander la permission
|
||||
// si l'utilisateur a indiqué vouloir recevoir des notifications.
|
||||
Log.w(TAG, "Permission POST_NOTIFICATIONS manquante pour API 33+. Notification ID " + notificationId + " non affichée.");
|
||||
// Optionnel : Envoyer un broadcast ou utiliser une autre méthode pour signaler l'échec ? Non, simple log suffit ici.
|
||||
return;
|
||||
} else {
|
||||
Log.d(TAG, "Permission POST_NOTIFICATIONS accordée sur API 33+.");
|
||||
// Si la permission n'est pas accordée, on ne peut pas afficher la notification.
|
||||
// L'application devrait demander la permission à l'utilisateur à un moment approprié.
|
||||
return; // Arrête l'exécution de la méthode
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "API < 33, permission POST_NOTIFICATIONS non requise pour afficher.");
|
||||
}
|
||||
|
||||
// --- Création de l'Intent pour le clic sur la notification ---
|
||||
// Ouvre MainActivity lorsque l'utilisateur clique sur la notification.
|
||||
// Crée un Intent pour ouvrir MainActivity lorsque la notification est cliquée
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
// Flags pour gérer correctement la pile d'activités (ramène l'instance existante ou en crée une nouvelle)
|
||||
// Flags pour gérer le comportement de la pile d'activités
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
// Flags pour le PendingIntent (requis : IMMUTABLE sur API 31+)
|
||||
|
||||
// Crée un PendingIntent qui enveloppe l'Intent
|
||||
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
// Crée le PendingIntent qui enveloppe l'Intent
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, pendingIntentFlags);
|
||||
|
||||
// --- Construction de la notification via NotificationCompat ---
|
||||
// Construit la notification en utilisant NotificationCompat.Builder pour la compatibilité
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Icône obligatoire (doit être monochrome/blanche avec transparence)
|
||||
// .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher)) // Icône optionnelle plus grande
|
||||
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Icône obligatoire
|
||||
.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é)
|
||||
.setContentText(message) // Corps du message
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité (affecte l'affichage)
|
||||
.setContentIntent(pendingIntent) // Action lorsque l'utilisateur clique sur la notification
|
||||
.setAutoCancel(true) // Ferme la notification après le clic
|
||||
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) // Son et vibration par défaut
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); // Visible sur l'écran de verrouillage
|
||||
|
||||
// --- Affichage de la notification ---
|
||||
// Utilise NotificationManagerCompat pour la compatibilité
|
||||
// Récupère NotificationManagerCompat pour envoyer la notification
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
try {
|
||||
// Affiche la notification. Si une notification avec le même ID existe déjà, elle est mise à jour.
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
Log.i(TAG, "Notification ID " + notificationId + " affichée avec succès.");
|
||||
} catch (SecurityException e){
|
||||
// Gérer l'exception de sécurité qui peut (rarement) survenir même avec la vérification ci-dessus.
|
||||
Log.e(TAG, "Erreur de sécurité lors de l'affichage de la notification ID " + notificationId, e);
|
||||
// Afficher un Toast ou logguer est généralement suffisant ici.
|
||||
} catch (Exception ex) {
|
||||
// Capturer d'autres exceptions potentielles
|
||||
Log.e(TAG, "Erreur inattendue lors de l'affichage de la notification ID " + notificationId, ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
/**
|
||||
* Service exécuté en arrière-plan pour tenter d'envoyer des notifications périodiques,
|
||||
* comme un rappel du meilleur score ou un rappel d'inactivité.
|
||||
*
|
||||
* <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;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper; // Important pour créer un Handler sur le Main Thread
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class NotificationService extends Service {
|
||||
|
||||
/** Tag pour les logs spécifiques à ce service. */
|
||||
private static final String TAG = "NotificationService";
|
||||
|
||||
// --- Constantes de Notification et Intervalles ---
|
||||
/** ID unique pour la notification de rappel du meilleur score. */
|
||||
private static final int NOTIFICATION_ID_HIGHSCORE = 2; // Doit être différent des autres notifs (ex: 1 pour achievement)
|
||||
/** ID unique pour la notification de rappel d'inactivité. */
|
||||
private static final int NOTIFICATION_ID_INACTIVITY = 3;
|
||||
|
||||
/** Intervalle minimum souhaité entre deux notifications de rappel du meilleur score (ex: 1 jour). */
|
||||
private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
|
||||
/** Seuil d'inactivité : durée après laquelle une notification d'inactivité peut être envoyée (ex: 3 jours). */
|
||||
private static final long INACTIVITY_THRESHOLD_MS = TimeUnit.DAYS.toMillis(3);
|
||||
/** Intervalle minimum souhaité entre deux notifications de rappel d'inactivité (ex: 1 jour, pour éviter spam si toujours inactif). */
|
||||
private static final long INACTIVITY_NOTIFICATION_COOLDOWN_MS = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
/** Intervalle auquel le Handler vérifie s'il faut envoyer une notification (ex: toutes les 6 heures). */
|
||||
// NOTE: Avec WorkManager, cette vérification fréquente serait inutile, on planifierait directement la tâche au bon moment.
|
||||
private static final long CHECK_INTERVAL_MS = TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
/** Handler pour poster les tâches Runnable périodiques. Fonctionne sur le thread principal. */
|
||||
private Handler handler;
|
||||
/** Runnable contenant la logique de vérification et d'envoi des notifications. */
|
||||
private Runnable periodicTaskRunnable;
|
||||
|
||||
// --- Constantes SharedPreferences ---
|
||||
// (Doivent correspondre à celles utilisées dans MainActivity et GameStats)
|
||||
/** Nom du fichier de préférences partagées. */
|
||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||
/** Clé pour lire le meilleur score global. */
|
||||
private static final String HIGH_SCORE_KEY = "high_score";
|
||||
/** Clé pour lire le timestamp de la dernière partie jouée. */
|
||||
private static final String LAST_PLAYED_TIME_KEY = "last_played_time";
|
||||
/** Clé pour vérifier si les notifications sont activées par l'utilisateur. */
|
||||
private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled";
|
||||
/** Clé pour stocker le timestamp du dernier envoi de la notification High Score. */
|
||||
private static final String LAST_HS_NOTIFICATION_TIME = "lastHsNotificationTime";
|
||||
/** Clé pour stocker le timestamp du dernier envoi de la notification d'Inactivité. */
|
||||
private static final String LAST_INACTIVITY_NOTIFICATION_TIME = "lastInactivityNotificationTime";
|
||||
|
||||
/**
|
||||
* Appelée par le système lors de la première création du service.
|
||||
* Initialise le Handler et le Runnable pour la tâche périodique.
|
||||
* Ne crée PAS le canal de notification ici, car MainActivity le fait déjà.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate: Service de notification en cours de création.");
|
||||
// Utilise le Looper principal pour le Handler. Simple, mais bloque le thread principal
|
||||
// si checkAndSendNotifications() devient une tâche longue.
|
||||
// Pour des tâches potentiellement longues, utiliser un HandlerThread serait mieux.
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// Définit le Runnable qui sera exécuté périodiquement
|
||||
periodicTaskRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "Exécution de la tâche périodique de vérification des notifications.");
|
||||
// Vérifie s'il faut envoyer des notifications
|
||||
checkAndSendNotifications();
|
||||
// Replanifie la prochaine exécution de cette même tâche
|
||||
// ATTENTION: Ce mécanisme n'est pas fiable en arrière-plan.
|
||||
handler.postDelayed(this, CHECK_INTERVAL_MS);
|
||||
Log.d(TAG, "Prochaine vérification des notifications planifiée dans " + CHECK_INTERVAL_MS + " ms.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelée lorsque le service est démarré, soit par `startService()` soit après un redémarrage du système
|
||||
* (si START_STICKY est utilisé et que le système choisit de le redémarrer).
|
||||
* Lance la tâche périodique de vérification des notifications.
|
||||
*
|
||||
* @param intent L'Intent fourni à startService(), peut être null si redémarré par le système.
|
||||
* @param flags Indicateurs supplémentaires sur la demande de démarrage.
|
||||
* @param startId Un identifiant unique représentant cette demande de démarrage spécifique.
|
||||
* @return La valeur de retour indique comment le système doit gérer le service s'il est tué.
|
||||
* {@code START_STICKY} : Le système essaiera de recréer le service après l'avoir tué,
|
||||
* mais l'Intent ne sera pas redélivré (il sera null).
|
||||
* <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) {
|
||||
Log.i(TAG, "onStartCommand: Service démarré (ou redémarré). StartId: " + startId);
|
||||
|
||||
// S'assure qu'il n'y a pas d'anciennes tâches en double avant de poster la nouvelle
|
||||
handler.removeCallbacks(periodicTaskRunnable);
|
||||
// Lance la première exécution de la tâche immédiatement (ou presque)
|
||||
handler.post(periodicTaskRunnable);
|
||||
Log.d(TAG, "Première vérification des notifications postée.");
|
||||
|
||||
// Utiliser START_STICKY pour que le système essaie de redémarrer le service s'il est tué.
|
||||
// C'est un pis-aller par rapport à WorkManager.
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelée lorsque le service est sur le point d'être détruit.
|
||||
* Nettoie les ressources, en particulier, arrête la planification des tâches
|
||||
* périodiques via le Handler pour éviter les fuites ou les exécutions non désirées.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.i(TAG, "onDestroy: Service de notification en cours de destruction.");
|
||||
// Arrête la planification des tâches lorsque le service est détruit
|
||||
if (handler != null && periodicTaskRunnable != null) {
|
||||
Log.d(TAG, "Annulation des tâches périodiques planifiées.");
|
||||
handler.removeCallbacks(periodicTaskRunnable);
|
||||
}
|
||||
// Autres nettoyages si nécessaire
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'interface de communication au client.
|
||||
* Comme c'est un service démarré (Started Service) et non lié (Bound Service),
|
||||
* cette méthode retourne {@code null}.
|
||||
*
|
||||
* @param intent L'Intent qui a été utilisé pour se lier au service (non pertinent ici).
|
||||
* @return null car ce service ne permet pas le binding.
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// Ce service n'est pas conçu pour être lié.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie les conditions pour envoyer les notifications périodiques (High Score, Inactivité).
|
||||
* Lit les préférences utilisateur (notifications activées?) et les timestamps nécessaires.
|
||||
* Appelle les méthodes d'affichage de notification appropriées si les conditions et
|
||||
* les délais depuis le dernier envoi sont respectés.
|
||||
* Met à jour le timestamp du dernier envoi après avoir tenté d'envoyer une notification.
|
||||
*/
|
||||
private void checkAndSendNotifications() {
|
||||
Log.d(TAG, "checkAndSendNotifications: Début de la vérification.");
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||
|
||||
// 1. Vérifier si les notifications sont globalement activées par l'utilisateur
|
||||
boolean notificationsEnabled = prefs.getBoolean(NOTIFICATIONS_ENABLED_KEY, false); // Lire la préférence
|
||||
if (!notificationsEnabled) {
|
||||
Log.i(TAG, "checkAndSendNotifications: Notifications désactivées dans les préférences, arrêt de la vérification.");
|
||||
// Si les notifications sont désactivées, on pourrait arrêter le service pour économiser des ressources.
|
||||
// stopSelf(); // Arrêterait le service lui-même.
|
||||
return;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// 2. Vérification pour la notification High Score
|
||||
long lastHsNotificationTime = prefs.getLong(LAST_HS_NOTIFICATION_TIME, 0);
|
||||
if (currentTime - lastHsNotificationTime > HIGHSCORE_INTERVAL_MS) {
|
||||
Log.d(TAG, "checkAndSendNotifications: Intervalle pour notification High Score écoulé.");
|
||||
int highScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||||
if (highScore > 0) { // N'envoie pas si le high score est 0
|
||||
Log.i(TAG, "checkAndSendNotifications: Envoi de la notification High Score.");
|
||||
showHighScoreNotificationNow(highScore);
|
||||
// Met à jour le timestamp du dernier envoi APRÈS avoir tenté d'envoyer
|
||||
prefs.edit().putLong(LAST_HS_NOTIFICATION_TIME, currentTime).apply();
|
||||
} else {
|
||||
Log.d(TAG, "checkAndSendNotifications: High Score est 0, notification non envoyée.");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "checkAndSendNotifications: Intervalle pour notification High Score non écoulé.");
|
||||
}
|
||||
|
||||
// 3. Vérification pour la notification d'Inactivité
|
||||
long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0);
|
||||
long lastInactivityNotificationTime = prefs.getLong(LAST_INACTIVITY_NOTIFICATION_TIME, 0);
|
||||
|
||||
// Condition 1: Temps depuis la dernière partie > Seuil d'inactivité
|
||||
boolean isInactivityThresholdMet = (lastPlayedTime > 0 && currentTime - lastPlayedTime > INACTIVITY_THRESHOLD_MS);
|
||||
// Condition 2: Temps depuis la dernière notification d'inactivité > Cooldown
|
||||
boolean isCooldownMet = (currentTime - lastInactivityNotificationTime > INACTIVITY_NOTIFICATION_COOLDOWN_MS);
|
||||
|
||||
if (isInactivityThresholdMet) {
|
||||
Log.d(TAG, "checkAndSendNotifications: Seuil d'inactivité atteint.");
|
||||
if (isCooldownMet) {
|
||||
Log.i(TAG, "checkAndSendNotifications: Cooldown pour notification d'inactivité respecté. Envoi...");
|
||||
showInactivityNotificationNow();
|
||||
// Met à jour le timestamp du dernier envoi APRÈS avoir tenté d'envoyer
|
||||
prefs.edit().putLong(LAST_INACTIVITY_NOTIFICATION_TIME, currentTime).apply();
|
||||
} else {
|
||||
Log.d(TAG, "checkAndSendNotifications: Cooldown pour notification d'inactivité non écoulé.");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "checkAndSendNotifications: Seuil d'inactivité non atteint.");
|
||||
}
|
||||
Log.d(TAG, "checkAndSendNotifications: Fin de la vérification.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prépare et affiche immédiatement la notification de rappel du meilleur score.
|
||||
* Utilise {@link NotificationHelper} pour l'affichage effectif.
|
||||
*
|
||||
* @param highScore Le meilleur score à afficher dans la notification.
|
||||
*/
|
||||
private void showHighScoreNotificationNow(int highScore) {
|
||||
String title = getString(R.string.notification_title_highscore);
|
||||
String message = getString(R.string.notification_text_highscore, highScore);
|
||||
NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_HIGHSCORE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prépare et affiche immédiatement la notification de rappel d'inactivité.
|
||||
* Utilise {@link NotificationHelper} pour l'affichage effectif.
|
||||
*/
|
||||
private void showInactivityNotificationNow() {
|
||||
String title = getString(R.string.notification_title_inactivity);
|
||||
String message = getString(R.string.notification_text_inactivity);
|
||||
NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_INACTIVITY);
|
||||
}
|
||||
|
||||
}
|
148
app/src/main/java/legion/muyue/best2048/NotificationWorker.java
Normal file
148
app/src/main/java/legion/muyue/best2048/NotificationWorker.java
Normal file
@ -0,0 +1,148 @@
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Un {@link Worker} Android conçu pour s'exécuter périodiquement en arrière-plan
|
||||
* via {@link androidx.work.WorkManager}.
|
||||
* Sa tâche est de vérifier si certaines conditions sont remplies pour envoyer
|
||||
* des notifications à l'utilisateur, telles qu'un rappel de son meilleur score
|
||||
* ou une notification d'inactivité pour l'encourager à rejouer.
|
||||
* Les conditions, seuils et états (comme la dernière fois jouée ou notifiée)
|
||||
* sont gérés via {@link SharedPreferences}.
|
||||
*/
|
||||
public class NotificationWorker extends Worker {
|
||||
|
||||
// --- Constantes ---
|
||||
|
||||
/** Identifiant unique pour la notification de rappel du meilleur score. */
|
||||
private static final int NOTIFICATION_ID_HIGHSCORE = 2;
|
||||
/** Identifiant unique pour la notification d'inactivité. */
|
||||
private static final int NOTIFICATION_ID_INACTIVITY = 3;
|
||||
|
||||
/** Intervalle minimum (en millisecondes) entre deux notifications de rappel du meilleur score. (1 jour) */
|
||||
private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
|
||||
/** Seuil d'inactivité (en millisecondes) : durée depuis la dernière partie jouée au-delà de laquelle une notification peut être envoyée. (3 jours) */
|
||||
private static final long INACTIVITY_THRESHOLD_MS = TimeUnit.DAYS.toMillis(3);
|
||||
/** Temps de recharge minimum (en millisecondes) entre deux notifications d'inactivité. (1 jour) */
|
||||
private static final long INACTIVITY_NOTIFICATION_COOLDOWN_MS = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
/** Nom du fichier SharedPreferences utilisé par cette classe et potentiellement d'autres (ex: GameStats). */
|
||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||
/** Clé SharedPreferences pour le meilleur score (utilisé aussi par GameStats). */
|
||||
private static final String HIGH_SCORE_KEY = "high_score";
|
||||
/** Clé SharedPreferences pour stocker le timestamp de la dernière fois où l'utilisateur a joué. */
|
||||
private static final String LAST_PLAYED_TIME_KEY = "last_played_time";
|
||||
/** Clé SharedPreferences pour l'activation globale des notifications par l'utilisateur. */
|
||||
private static final String NOTIFICATIONS_ENABLED_KEY = "notifications_enabled";
|
||||
/** Clé SharedPreferences pour stocker le timestamp de la dernière notification de meilleur score envoyée. */
|
||||
private static final String LAST_HS_NOTIFICATION_TIME = "lastHsNotificationTime";
|
||||
/** Clé SharedPreferences pour stocker le timestamp de la dernière notification d'inactivité envoyée. */
|
||||
private static final String LAST_INACTIVITY_NOTIFICATION_TIME = "lastInactivityNotificationTime";
|
||||
|
||||
/**
|
||||
* Constructeur standard pour un {@link Worker}.
|
||||
*
|
||||
* @param context Le contexte de l'application.
|
||||
* @param workerParams Paramètres pour le Worker, fournis par WorkManager.
|
||||
*/
|
||||
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute la tâche principale du Worker en arrière-plan.
|
||||
* Vérifie si les notifications sont activées, puis évalue les conditions pour
|
||||
* envoyer une notification de meilleur score ou une notification d'inactivité.
|
||||
* Met à jour les timestamps des dernières notifications envoyées si nécessaire.
|
||||
* Cette méthode est appelée par WorkManager sur un thread d'arrière-plan.
|
||||
*
|
||||
* @return {@link Result#success()} si le travail s'est terminé (qu'une notification ait été envoyée ou non).
|
||||
* Retourne {@link Result#failure()} ou {@link Result#retry()} en cas d'erreur non récupérable ou si le travail doit être retenté.
|
||||
* Ici, on retourne toujours success() même en cas d'exception interne pour ne pas bloquer les exécutions futures,
|
||||
* mais un logging serait approprié.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Context context = getApplicationContext();
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
// 1. Vérifier si les notifications sont activées globalement
|
||||
boolean notificationsEnabled = prefs.getBoolean(NOTIFICATIONS_ENABLED_KEY, false);
|
||||
if (!notificationsEnabled) {
|
||||
return Result.success(); // Travail terminé avec succès (rien à faire)
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// 2. Vérification pour la notification du meilleur score
|
||||
try {
|
||||
long lastHsNotificationTime = prefs.getLong(LAST_HS_NOTIFICATION_TIME, 0);
|
||||
// Vérifier si l'intervalle depuis la dernière notification de high score est dépassé
|
||||
if (currentTime - lastHsNotificationTime > HIGHSCORE_INTERVAL_MS) {
|
||||
int highScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||||
// Vérifier s'il y a un high score à afficher (> 0)
|
||||
if (highScore > 0) {
|
||||
showHighScoreNotificationNow(context, highScore);
|
||||
// Mettre à jour le timestamp de la dernière notification de high score
|
||||
prefs.edit().putLong(LAST_HS_NOTIFICATION_TIME, currentTime).apply();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
// 3. Vérification pour la notification d'inactivité
|
||||
try {
|
||||
long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0);
|
||||
long lastInactivityNotificationTime = prefs.getLong(LAST_INACTIVITY_NOTIFICATION_TIME, 0);
|
||||
|
||||
// Condition 1: L'utilisateur n'a pas joué depuis un certain temps (INACTIVITY_THRESHOLD_MS)
|
||||
boolean isInactivityThresholdMet = (lastPlayedTime > 0 && currentTime - lastPlayedTime > INACTIVITY_THRESHOLD_MS);
|
||||
// Condition 2: Assez de temps s'est écoulé depuis la dernière notification d'inactivité (cooldown)
|
||||
boolean isCooldownMet = (currentTime - lastInactivityNotificationTime > INACTIVITY_NOTIFICATION_COOLDOWN_MS);
|
||||
|
||||
if (isInactivityThresholdMet && isCooldownMet) {
|
||||
showInactivityNotificationNow(context);
|
||||
// Mettre à jour le timestamp de la dernière notification d'inactivité
|
||||
prefs.edit().putLong(LAST_INACTIVITY_NOTIFICATION_TIME, currentTime).apply();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit et affiche immédiatement la notification de rappel du meilleur score.
|
||||
* Utilise une classe helper {@code NotificationHelper} (non fournie ici) pour la logique d'affichage réelle.
|
||||
* Récupère les textes depuis les ressources de chaînes Android (R.string).
|
||||
*
|
||||
* @param context Le contexte applicatif.
|
||||
* @param highScore Le meilleur score actuel de l'utilisateur à afficher.
|
||||
*/
|
||||
private void showHighScoreNotificationNow(Context context, int highScore) {
|
||||
String title = context.getString(R.string.notification_title_highscore);
|
||||
String message = context.getString(R.string.notification_text_highscore, highScore);
|
||||
NotificationHelper.showNotification(context, title, message, NOTIFICATION_ID_HIGHSCORE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit et affiche immédiatement la notification d'inactivité.
|
||||
* Utilise une classe helper {@code NotificationHelper} (non fournie ici) pour la logique d'affichage réelle.
|
||||
* Récupère les textes depuis les ressources de chaînes Android (R.string).
|
||||
*
|
||||
* @param context Le contexte applicatif.
|
||||
*/
|
||||
private void showInactivityNotificationNow(Context context) {
|
||||
String title = context.getString(R.string.notification_title_inactivity);
|
||||
String message = context.getString(R.string.notification_text_inactivity);
|
||||
NotificationHelper.showNotification(context, title, message, NOTIFICATION_ID_INACTIVITY);
|
||||
}
|
||||
}
|
@ -1,9 +1,3 @@
|
||||
// Fichier OnSwipeTouchListener.java
|
||||
/**
|
||||
* Listener de vue personnalisé qui détecte les gestes de balayage (swipe)
|
||||
* dans les quatre directions cardinales et notifie un listener externe.
|
||||
* Utilise {@link GestureDetector} pour l'analyse des gestes.
|
||||
*/
|
||||
package legion.muyue.best2048;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@ -12,66 +6,85 @@ import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Un {@link View.OnTouchListener} qui détecte les gestes de balayage (swipe)
|
||||
* dans quatre directions (haut, bas, gauche, droite) sur une {@link View} associée.
|
||||
* Utilise un {@link GestureDetector} pour interpréter les événements tactiles.
|
||||
* Notifie un {@link SwipeListener} lorsqu'un balayage valide est détecté.
|
||||
*
|
||||
* Pour l'utiliser, créez une instance de cette classe en fournissant un {@link Context}
|
||||
* et une implémentation de {@link SwipeListener}, puis attachez-la à la View souhaitée
|
||||
* via {@link View#setOnTouchListener(View.OnTouchListener)}.
|
||||
*/
|
||||
public class OnSwipeTouchListener implements View.OnTouchListener {
|
||||
|
||||
/** Détecteur de gestes standard d'Android. */
|
||||
/** Détecteur de gestes Android utilisé pour interpréter les événements tactiles. */
|
||||
private final GestureDetector gestureDetector;
|
||||
/** Listener externe à notifier lors de la détection d'un swipe. */
|
||||
/** L'écouteur qui sera notifié des événements de balayage détectés. */
|
||||
private final SwipeListener listener;
|
||||
|
||||
/**
|
||||
* Interface à implémenter par les classes souhaitant réagir aux événements de swipe.
|
||||
* Interface de callback à implémenter par les classes qui souhaitent être notifiées
|
||||
* des événements de balayage détectés par {@link OnSwipeTouchListener}.
|
||||
*/
|
||||
public interface SwipeListener {
|
||||
/** Appelée lorsqu'un swipe vers le haut est détecté. */
|
||||
/** Appelé lorsqu'un balayage vers le haut est détecté. */
|
||||
void onSwipeTop();
|
||||
/** Appelée lorsqu'un swipe vers le bas est détecté. */
|
||||
/** Appelé lorsqu'un balayage vers le bas est détecté. */
|
||||
void onSwipeBottom();
|
||||
/** Appelée lorsqu'un swipe vers la gauche est détecté. */
|
||||
/** Appelé lorsqu'un balayage vers la gauche est détecté. */
|
||||
void onSwipeLeft();
|
||||
/** Appelée lorsqu'un swipe vers la droite est détecté. */
|
||||
/** Appelé lorsqu'un balayage vers la droite est détecté. */
|
||||
void onSwipeRight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur.
|
||||
* @param context Contexte applicatif, nécessaire pour `GestureDetector`.
|
||||
* @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null.
|
||||
* Construit une nouvelle instance de l'écouteur de balayage.
|
||||
*
|
||||
* @param context Le contexte de l'application ou de l'activité, nécessaire pour initialiser le {@link GestureDetector}.
|
||||
* @param listener L'instance de {@link SwipeListener} qui recevra les notifications de balayage. Ne doit pas être null.
|
||||
*/
|
||||
public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) {
|
||||
this.gestureDetector = new GestureDetector(context, new GestureListener());
|
||||
// Stocke la référence vers l'écouteur fourni
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepte les événements tactiles sur la vue associée et les délègue
|
||||
* au {@link GestureDetector} pour analyse.
|
||||
* @param v La vue touchée.
|
||||
* @param event L'événement tactile.
|
||||
* @return true si le geste a été consommé par le détecteur, false sinon.
|
||||
* Méthode appelée lorsque la {@link View} associée reçoit un événement tactile.
|
||||
* Délègue le traitement de l'événement au {@link GestureDetector}.
|
||||
*
|
||||
* @param v La {@link View} qui a reçu l'événement tactile.
|
||||
* @param event L'objet {@link MotionEvent} décrivant l'événement tactile.
|
||||
* @return {@code true} si l'événement a été consommé par le {@link GestureDetector}, {@code false} sinon.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
// Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling),
|
||||
// il retournera true, et l'événement ne sera pas propagé davantage.
|
||||
// Passe l'événement tactile au GestureDetector pour analyse
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide).
|
||||
* Classe interne qui étend {@link GestureDetector.SimpleOnGestureListener} pour
|
||||
* implémenter la logique de détection de balayage (spécifiquement dans {@code onFling}).
|
||||
*/
|
||||
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
/** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */
|
||||
/** Distance minimale (en pixels) qu'un doigt doit parcourir pour qu'un mouvement soit considéré comme un balayage. */
|
||||
private static final int SWIPE_THRESHOLD = 100;
|
||||
/** Vitesse minimale (en pixels/sec) pour qu'un mouvement soit considéré comme un swipe. */
|
||||
/** Vitesse minimale (en pixels par seconde) requise pour qu'un mouvement soit considéré comme un balayage (fling). */
|
||||
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
|
||||
|
||||
/**
|
||||
* Toujours retourner true pour onDown garantit que les événements suivants
|
||||
* (comme onFling) seront bien reçus par ce listener.
|
||||
* Appelée lorsque l'événement {@link MotionEvent#ACTION_DOWN} se produit.
|
||||
* Doit retourner {@code true} pour indiquer que ce listener est intéressé
|
||||
* par la séquence complète des événements tactiles (move, up, fling).
|
||||
*
|
||||
* @param e L'événement MotionEvent initial (ACTION_DOWN).
|
||||
* @return Toujours {@code true}.
|
||||
*/
|
||||
@Override
|
||||
public boolean onDown(@NonNull MotionEvent e) {
|
||||
@ -79,47 +92,61 @@ public class OnSwipeTouchListener implements View.OnTouchListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelée quand un geste de 'fling' (balayage rapide) est détecté.
|
||||
* Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide
|
||||
* et notifie le {@link SwipeListener} externe.
|
||||
* Appelée lorsque le {@link GestureDetector} détecte un mouvement de "fling"
|
||||
* (un glissement rapide suivi d'un relâchement). C'est ici que la logique
|
||||
* de détection de balayage est implémentée.
|
||||
*
|
||||
* @param e1 L'événement {@link MotionEvent} initial (ACTION_DOWN) où le fling a commencé. Peut être null dans certains cas rares.
|
||||
* @param e2 L'événement {@link MotionEvent} final (ACTION_UP) où le fling s'est terminé.
|
||||
* @param velocityX La vélocité du fling sur l'axe X (pixels par seconde).
|
||||
* @param velocityY La vélocité du fling sur l'axe Y (pixels par seconde).
|
||||
* @return {@code true} si un balayage valide a été détecté et géré (c'est-à-dire qu'une méthode du listener a été appelée), {@code false} sinon.
|
||||
*/
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (e1 == null) return false; // Point de départ est nécessaire
|
||||
public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
||||
// Vérifie si l'événement initial est null (précaution)
|
||||
if (e1 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = false;
|
||||
boolean result = false; // Indique si un swipe a été détecté et traité
|
||||
try {
|
||||
// Calcule la différence de position entre le début et la fin du mouvement
|
||||
float diffY = e2.getY() - e1.getY();
|
||||
float diffX = e2.getX() - e1.getX();
|
||||
|
||||
// Priorité au mouvement le plus ample (horizontal ou vertical)
|
||||
// Détermine si le mouvement est principalement horizontal ou vertical
|
||||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||
// Mouvement principalement horizontal
|
||||
// Vérifie si la distance et la vitesse dépassent les seuils
|
||||
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (diffX > 0) {
|
||||
// Balayage vers la droite
|
||||
listener.onSwipeRight();
|
||||
} else {
|
||||
// Balayage vers la gauche
|
||||
listener.onSwipeLeft();
|
||||
}
|
||||
result = true; // Geste horizontal traité
|
||||
result = true; // Un balayage horizontal a été traité
|
||||
}
|
||||
} else {
|
||||
// Mouvement principalement vertical
|
||||
// Vérifie si la distance et la vitesse dépassent les seuils
|
||||
if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (diffY > 0) {
|
||||
// Balayage vers le bas
|
||||
listener.onSwipeBottom();
|
||||
} else {
|
||||
// Balayage vers le haut
|
||||
listener.onSwipeTop();
|
||||
}
|
||||
result = true; // Geste vertical traité
|
||||
result = true; // Un balayage vertical a été traité
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
// En cas d'erreur inattendue, on logue discrètement.
|
||||
System.err.println("Erreur dans OnSwipeTouchListener.onFling: " + exception.getMessage());
|
||||
// Ne pas crasher l'application pour une erreur de détection de geste.
|
||||
}
|
||||
// Retourne true si un swipe a été détecté et traité, false sinon
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} // Fin OnSwipeTouchListener
|
||||
}
|
@ -1,20 +1,86 @@
|
||||
package legion.muyue.best2048.data; // Créez un sous-package data si vous voulez
|
||||
package legion.muyue.best2048.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Représente les informations de base d'une partie du jeu 2048.
|
||||
* Cette classe est utilisée pour stocker et transférer les données relatives
|
||||
* à une session de jeu spécifique, notamment via la sérialisation/désérialisation JSON avec Gson.
|
||||
*/
|
||||
public class GameInfo {
|
||||
@SerializedName("gameId") // Correspond au nom du champ JSON
|
||||
|
||||
/**
|
||||
* L'identifiant unique de la partie.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "gameId".
|
||||
*/
|
||||
@SerializedName("gameId")
|
||||
private String gameId;
|
||||
@SerializedName("status") // Ex: WAITING, PLAYING, FINISHED
|
||||
|
||||
/**
|
||||
* Le statut actuel de la partie (par exemple, "en attente", "en cours", "terminée").
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "status".
|
||||
*/
|
||||
@SerializedName("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* L'identifiant du premier joueur.
|
||||
* Peut être null si le joueur n'a pas encore rejoint.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player1Id".
|
||||
*/
|
||||
@SerializedName("player1Id")
|
||||
private String player1Id;
|
||||
@SerializedName("player2Id")
|
||||
private String player2Id; // Peut être null si en attente
|
||||
|
||||
// --- Getters (et Setters si nécessaire) ---
|
||||
public String getGameId() { return gameId; }
|
||||
public String getStatus() { return status; }
|
||||
public String getPlayer1Id() { return player1Id; }
|
||||
public String getPlayer2Id() { return player2Id; }
|
||||
/**
|
||||
* L'identifiant du deuxième joueur.
|
||||
* Peut être null si le joueur n'a pas encore rejoint ou s'il s'agit d'une partie solo.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player2Id".
|
||||
*/
|
||||
@SerializedName("player2Id")
|
||||
private String player2Id;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
* Nécessaire pour certaines bibliothèques de sérialisation/désérialisation comme Gson.
|
||||
*/
|
||||
public GameInfo() {
|
||||
// Constructeur par défaut explicite pour la clarté et la compatibilité
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant unique de la partie.
|
||||
*
|
||||
* @return L'identifiant de la partie.
|
||||
*/
|
||||
public String getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le statut actuel de la partie.
|
||||
*
|
||||
* @return Le statut de la partie (ex: "waiting", "playing", "finished").
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du premier joueur.
|
||||
*
|
||||
* @return L'identifiant du joueur 1, ou null s'il n'est pas défini.
|
||||
*/
|
||||
public String getPlayer1Id() {
|
||||
return player1Id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du deuxième joueur.
|
||||
*
|
||||
* @return L'identifiant du joueur 2, ou null s'il n'est pas défini.
|
||||
*/
|
||||
public String getPlayer2Id() {
|
||||
return player2Id;
|
||||
}
|
||||
}
|
@ -1,64 +1,218 @@
|
||||
package legion.muyue.best2048.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Représente l'état complet d'une partie du jeu 2048 à un instant T.
|
||||
* Cette classe est typiquement utilisée pour encapsuler les données reçues
|
||||
* en réponse à une requête d'état de jeu, souvent via JSON (Gson).
|
||||
* Elle contient les informations sur le plateau, les scores, le joueur courant, etc.
|
||||
*/
|
||||
public class GameStateResponse {
|
||||
|
||||
/**
|
||||
* L'identifiant unique de la partie.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "gameId".
|
||||
*/
|
||||
@SerializedName("gameId")
|
||||
private String gameId;
|
||||
|
||||
/**
|
||||
* La grille de jeu actuelle, représentée par un tableau 2D d'entiers.
|
||||
* Chaque entier représente la valeur d'une tuile (0 pour une case vide).
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "board".
|
||||
*/
|
||||
@SerializedName("board")
|
||||
private int[][] board; // Plateau de jeu actuel
|
||||
private int[][] board;
|
||||
|
||||
/**
|
||||
* Le score actuel du joueur 1.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player1Score".
|
||||
*/
|
||||
@SerializedName("player1Score")
|
||||
private int player1Score;
|
||||
|
||||
/**
|
||||
* Le score actuel du joueur 2.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player2Score".
|
||||
*/
|
||||
@SerializedName("player2Score")
|
||||
private int player2Score;
|
||||
@SerializedName("currentPlayerId") // ID du joueur dont c'est le tour
|
||||
|
||||
/**
|
||||
* L'identifiant du joueur dont c'est actuellement le tour de jouer.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "currentPlayerId".
|
||||
*/
|
||||
@SerializedName("currentPlayerId")
|
||||
private String currentPlayerId;
|
||||
|
||||
/**
|
||||
* Indicateur booléen signalant si la partie est terminée.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "isGameOver".
|
||||
*/
|
||||
@SerializedName("isGameOver")
|
||||
private boolean isGameOver;
|
||||
@SerializedName("winnerId") // ID du gagnant si terminé, null sinon
|
||||
|
||||
/**
|
||||
* L'identifiant du joueur gagnant, si la partie est terminée et qu'il y a un gagnant.
|
||||
* Peut être null si la partie n'est pas terminée ou s'il y a égalité.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "winnerId".
|
||||
*/
|
||||
@SerializedName("winnerId")
|
||||
private String winnerId;
|
||||
|
||||
/**
|
||||
* Le statut global de la partie (par exemple, "en cours", "terminée").
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "status".
|
||||
*/
|
||||
@SerializedName("status")
|
||||
private String status;
|
||||
@SerializedName("player1Id") // Ajoute ce champ s'il manque
|
||||
|
||||
/**
|
||||
* L'identifiant du joueur 1.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player1Id".
|
||||
*/
|
||||
@SerializedName("player1Id")
|
||||
private String player1Id;
|
||||
@SerializedName("player2Id") // Ajoute ce champ s'il manque
|
||||
|
||||
/**
|
||||
* L'identifiant du joueur 2.
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "player2Id".
|
||||
*/
|
||||
@SerializedName("player2Id")
|
||||
private String player2Id;
|
||||
@SerializedName("targetScore") // Ajoute ce champ
|
||||
|
||||
/**
|
||||
* Le score cible à atteindre pour gagner la partie (par exemple, 2048).
|
||||
* Utilisé par Gson pour la sérialisation avec le nom "targetScore".
|
||||
*/
|
||||
@SerializedName("targetScore")
|
||||
private int targetScore;
|
||||
|
||||
// --- Getters ---
|
||||
|
||||
/**
|
||||
* Constructeur par défaut.
|
||||
* Nécessaire pour certaines bibliothèques de sérialisation/désérialisation comme Gson.
|
||||
*/
|
||||
public GameStateResponse() {
|
||||
// Constructeur par défaut explicite
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant unique de la partie.
|
||||
*
|
||||
* @return L'identifiant de la partie.
|
||||
*/
|
||||
public String getGameId() { return gameId; }
|
||||
|
||||
/**
|
||||
* Récupère la grille de jeu actuelle.
|
||||
* Le tableau retourné est une référence directe à l'état interne ;
|
||||
* pour éviter des modifications accidentelles, envisagez de retourner une copie.
|
||||
* Exemple de copie : {@code return Arrays.stream(board).map(int[]::clone).toArray(int[][]::new);}
|
||||
*
|
||||
* @return Un tableau 2D d'entiers représentant le plateau de jeu.
|
||||
*/
|
||||
public int[][] getBoard() { return board; }
|
||||
|
||||
/**
|
||||
* Récupère le score du joueur 1.
|
||||
*
|
||||
* @return Le score du joueur 1.
|
||||
*/
|
||||
public int getPlayer1Score() { return player1Score; }
|
||||
|
||||
/**
|
||||
* Récupère le score du joueur 2.
|
||||
*
|
||||
* @return Le score du joueur 2.
|
||||
*/
|
||||
public int getPlayer2Score() { return player2Score; }
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du joueur dont c'est le tour.
|
||||
*
|
||||
* @return L'identifiant du joueur courant.
|
||||
*/
|
||||
public String getCurrentPlayerId() { return currentPlayerId; }
|
||||
|
||||
/**
|
||||
* Vérifie si la partie est terminée.
|
||||
*
|
||||
* @return true si la partie est terminée, false sinon.
|
||||
*/
|
||||
public boolean isGameOver() { return isGameOver; }
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du joueur gagnant.
|
||||
*
|
||||
* @return L'identifiant du gagnant, ou null si la partie n'est pas terminée ou s'il n'y a pas de gagnant unique.
|
||||
*/
|
||||
public String getWinnerId() { return winnerId; }
|
||||
|
||||
/**
|
||||
* Récupère le statut actuel de la partie.
|
||||
*
|
||||
* @return Le statut de la partie (ex: "playing", "finished").
|
||||
*/
|
||||
public String getStatus() { return status; }
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du joueur 1.
|
||||
*
|
||||
* @return L'identifiant du joueur 1.
|
||||
*/
|
||||
public String getPlayer1Id() { return player1Id; }
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant du joueur 2.
|
||||
*
|
||||
* @return L'identifiant du joueur 2.
|
||||
*/
|
||||
public String getPlayer2Id() { return player2Id; }
|
||||
|
||||
/**
|
||||
* Récupère le score cible à atteindre pour gagner.
|
||||
*
|
||||
* @return Le score cible (par exemple, 2048).
|
||||
*/
|
||||
public int getTargetScore() { return targetScore; }
|
||||
|
||||
// --- Méthode utilitaire pour obtenir le score de l'adversaire ---
|
||||
|
||||
/**
|
||||
* Calcule et retourne le score de l'adversaire par rapport à l'identifiant du joueur fourni.
|
||||
*
|
||||
* @param myActualPlayerId L'identifiant du joueur pour lequel on veut connaître le score de l'adversaire.
|
||||
* @return Le score de l'adversaire si {@code myActualPlayerId} correspond à l'un des joueurs, sinon 0. Retourne 0 si {@code myActualPlayerId} est null.
|
||||
*/
|
||||
public int getOpponentScore(String myActualPlayerId) {
|
||||
if (myActualPlayerId == null) return 0;
|
||||
if (myActualPlayerId == null) {
|
||||
return 0;
|
||||
}
|
||||
if (myActualPlayerId.equals(player1Id)) {
|
||||
// Si je suis P1, le score de l'adversaire est P2
|
||||
return player2Score;
|
||||
} else if (myActualPlayerId.equals(player2Id)) {
|
||||
// Si je suis P2, le score de l'adversaire est P1
|
||||
return player1Score;
|
||||
}
|
||||
return 0; // Mon ID ne correspond à aucun joueur ?
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule et retourne le score du joueur correspondant à l'identifiant fourni.
|
||||
*
|
||||
* @param myActualPlayerId L'identifiant du joueur dont on veut connaître le score.
|
||||
* @return Le score du joueur si {@code myActualPlayerId} correspond à l'un des joueurs, sinon 0. Retourne 0 si {@code myActualPlayerId} est null.
|
||||
*/
|
||||
public int getMyScore(String myActualPlayerId) {
|
||||
if (myActualPlayerId == null) return 0;
|
||||
if (myActualPlayerId == null) {
|
||||
return 0;
|
||||
}
|
||||
if (myActualPlayerId.equals(player1Id)) {
|
||||
return player1Score;
|
||||
} else if (myActualPlayerId.equals(player2Id)) {
|
||||
return player2Score;
|
||||
}
|
||||
return 0; // Mon ID ne correspond à aucun joueur ?
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,12 +1,39 @@
|
||||
package legion.muyue.best2048.data;
|
||||
|
||||
/**
|
||||
* Représente une requête pour effectuer un mouvement dans le jeu 2048.
|
||||
* Cette classe est typiquement utilisée comme un objet de transfert de données (DTO)
|
||||
* pour envoyer l'action d'un joueur (la direction du mouvement) au serveur ou
|
||||
* au moteur de jeu.
|
||||
* Elle contient l'identifiant du joueur effectuant le mouvement et la direction choisie.
|
||||
*/
|
||||
public class MoveRequest {
|
||||
private String direction; // "UP", "DOWN", "LEFT", "RIGHT"
|
||||
private String playerId; // ID du joueur qui fait le mouvement
|
||||
|
||||
/**
|
||||
* La direction du mouvement demandé.
|
||||
* Par exemple : "UP", "DOWN", "LEFT", "RIGHT".
|
||||
* La casse et les valeurs exactes dépendent de la convention utilisée par le système.
|
||||
* Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication.
|
||||
*/
|
||||
private String direction;
|
||||
|
||||
/**
|
||||
* L'identifiant unique du joueur qui effectue le mouvement.
|
||||
* Permet au système de savoir quel joueur est à l'origine de la requête.
|
||||
* Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication.
|
||||
*/
|
||||
private String playerId;
|
||||
|
||||
/**
|
||||
* Construit une nouvelle requête de mouvement.
|
||||
*
|
||||
* @param direction La direction souhaitée pour le mouvement (ex: "UP", "DOWN", "LEFT", "RIGHT").
|
||||
* Ne doit généralement pas être null ou vide.
|
||||
* @param playerId L'identifiant du joueur effectuant la requête.
|
||||
* Ne doit généralement pas être null ou vide.
|
||||
*/
|
||||
public MoveRequest(String direction, String playerId) {
|
||||
this.direction = direction;
|
||||
this.playerId = playerId;
|
||||
}
|
||||
// Pas besoin de getters si seulement utilisé pour l'envoi avec Gson
|
||||
}
|
@ -1,10 +1,27 @@
|
||||
package legion.muyue.best2048.data;
|
||||
|
||||
/**
|
||||
* Représente une requête contenant uniquement l'identifiant d'un joueur.
|
||||
* Cette classe est typiquement utilisée comme un objet de transfert de données (DTO)
|
||||
* simple pour les opérations où seul l'identifiant du joueur est nécessaire,
|
||||
* par exemple, pour rejoindre une partie, demander des informations spécifiques
|
||||
* au joueur, ou s'identifier auprès d'un service.
|
||||
*/
|
||||
public class PlayerIdRequest {
|
||||
|
||||
/**
|
||||
* L'identifiant unique du joueur concerné par la requête.
|
||||
* Ce champ est destiné à être sérialisé (par exemple en JSON) pour la communication.
|
||||
*/
|
||||
private String playerId;
|
||||
|
||||
/**
|
||||
* Construit une nouvelle requête contenant un identifiant de joueur.
|
||||
*
|
||||
* @param playerId L'identifiant unique du joueur.
|
||||
* Ne doit généralement pas être null ou vide.
|
||||
*/
|
||||
public PlayerIdRequest(String playerId) {
|
||||
this.playerId = playerId;
|
||||
}
|
||||
// Pas besoin de getters si seulement utilisé pour l'envoi avec Gson
|
||||
}
|
@ -5,43 +5,78 @@ import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
* Fournit un client Retrofit configuré pour interagir avec l'API du jeu Best2048.
|
||||
* Cette classe utilise un modèle Singleton (paresseux et non strictement thread-safe dans sa forme actuelle)
|
||||
* pour l'instance Retrofit, garantissant qu'une seule instance est créée et réutilisée
|
||||
* pour toutes les requêtes réseau.
|
||||
* La configuration inclut l'URL de base de l'API, un intercepteur pour logger les requêtes/réponses
|
||||
* (niveau BODY), et un convertisseur Gson pour la sérialisation/désérialisation JSON.
|
||||
*/
|
||||
public class ApiClient {
|
||||
|
||||
// URL de base de votre API serveur
|
||||
/**
|
||||
* L'URL de base pour l'API du jeu Best2048.
|
||||
* Toutes les requêtes définies dans {@link ApiService} seront relatives à cette URL.
|
||||
*/
|
||||
private static final String BASE_URL = "https://best2048.legion-muyue.fr/api/";
|
||||
|
||||
/**
|
||||
* L'instance Singleton de Retrofit.
|
||||
* Initialisée paresseusement lors du premier appel à {@link #getClient()}.
|
||||
* Note: L'initialisation paresseuse ici n'est pas garantie comme étant thread-safe
|
||||
* dans des scénarios de haute concurrence sans synchronisation externe.
|
||||
*/
|
||||
private static Retrofit retrofit = null;
|
||||
|
||||
/**
|
||||
* Crée et retourne une instance singleton de Retrofit configurée.
|
||||
* Inclut un intercepteur pour logger les requêtes/réponses HTTP (utile pour le debug).
|
||||
* Constructeur privé pour empêcher l'instanciation directe de cette classe utilitaire.
|
||||
*/
|
||||
private ApiClient() {
|
||||
// Classe utilitaire, ne doit pas être instanciée.
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'instance Singleton de Retrofit.
|
||||
* Si l'instance n'existe pas encore, elle est créée et configurée avec :
|
||||
* <ul>
|
||||
* <li>L'URL de base ({@link #BASE_URL}).</li>
|
||||
* <li>Un {@link OkHttpClient} personnalisé incluant un {@link HttpLoggingInterceptor}
|
||||
* (niveau BODY) pour le débogage réseau.</li>
|
||||
* <li>Un {@link GsonConverterFactory} pour gérer la conversion JSON.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return L'instance configurée de Retrofit.
|
||||
* @return L'instance Retrofit configurée et prête à l'emploi.
|
||||
* @see #getApiService() pour obtenir directement une instance de service API.
|
||||
*/
|
||||
public static Retrofit getClient() {
|
||||
if (retrofit == null) {
|
||||
// Intercepteur pour voir les logs HTTP dans Logcat (Niveau BODY pour tout voir)
|
||||
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
// Client OkHttp avec l'intercepteur
|
||||
// Construit le client OkHttp en ajoutant l'intercepteur
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(logging)
|
||||
.build();
|
||||
|
||||
// Construction de l'instance Retrofit
|
||||
// Construit l'instance Retrofit
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client) // Utilise le client OkHttp configuré
|
||||
.addConverterFactory(GsonConverterFactory.create()) // Utilise Gson pour parser le JSON
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
}
|
||||
return retrofit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fournit une instance de l'interface ApiService.
|
||||
* @return Instance de ApiService.
|
||||
* Crée et retourne une instance de l'interface {@link ApiService}.
|
||||
* Utilise l'instance Retrofit obtenue via {@link #getClient()} pour générer
|
||||
* l'implémentation du service API.
|
||||
*
|
||||
* @return Une instance prête à l'emploi de {@link ApiService} pour effectuer des appels API.
|
||||
* Retourne une nouvelle instance de service à chaque appel, mais basée sur
|
||||
* le même client Retrofit/OkHttp sous-jacent.
|
||||
*/
|
||||
public static ApiService getApiService() {
|
||||
return getClient().create(ApiService.class);
|
||||
|
@ -1,40 +1,65 @@
|
||||
package legion.muyue.best2048.network; // Créez un sous-package network
|
||||
package legion.muyue.best2048.network;
|
||||
|
||||
import legion.muyue.best2048.data.GameInfo;
|
||||
import legion.muyue.best2048.data.GameStateResponse;
|
||||
import legion.muyue.best2048.data.MoveRequest;
|
||||
import legion.muyue.best2048.data.PlayerIdRequest;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query; // Pour éventuels paramètres de création
|
||||
import legion.muyue.best2048.data.PlayerIdRequest;
|
||||
|
||||
/**
|
||||
* Définit les points de terminaison (endpoints) de l'API REST pour le jeu Best2048.
|
||||
* Cette interface est utilisée par Retrofit pour générer une implémentation
|
||||
* capable d'effectuer des appels réseau vers le serveur du jeu.
|
||||
*
|
||||
* Les appels retournent des objets {@link Call} qui permettent une exécution
|
||||
* asynchrone (ou synchrone) des requêtes HTTP.
|
||||
*
|
||||
* @see ApiClient#getApiService() pour obtenir une instance implémentant cette interface.
|
||||
*/
|
||||
public interface ApiService {
|
||||
|
||||
/**
|
||||
* Crée une nouvelle partie ou rejoint une partie en attente (matchmaking simple).
|
||||
* TODO: Définir les paramètres nécessaires (ex: ID du joueur).
|
||||
* @return Informations sur la partie créée/rejointe.
|
||||
* Crée une nouvelle partie ou rejoint une partie existante en attente pour le joueur spécifié.
|
||||
* Effectue une requête POST vers {@code /api/games}.
|
||||
* Le corps de la requête contient l'identifiant du joueur.
|
||||
*
|
||||
* @param playerIdRequest Un objet {@link PlayerIdRequest} contenant l'identifiant unique
|
||||
* du joueur qui souhaite créer ou rejoindre une partie.
|
||||
* @return Un objet {@link Call} qui, en cas de succès, encapsule les informations
|
||||
* de la partie créée ou rejointe ({@link GameInfo}).
|
||||
*/
|
||||
@POST("games") // Endpoint: /api/games (POST)
|
||||
@POST("games")
|
||||
Call<GameInfo> createOrJoinGame(@Body PlayerIdRequest playerIdRequest);
|
||||
|
||||
/**
|
||||
* Récupère l'état actuel complet d'une partie spécifique.
|
||||
* @param gameId L'identifiant unique de la partie.
|
||||
* @return L'état actuel du jeu.
|
||||
* Effectue une requête GET vers {@code /api/games/{gameId}}.
|
||||
*
|
||||
* @param gameId L'identifiant unique de la partie dont l'état doit être récupéré.
|
||||
* Ce paramètre est inséré dans le chemin de l'URL.
|
||||
* @return Un objet {@link Call} qui, en cas de succès, encapsule l'état complet
|
||||
* de la partie demandée ({@link GameStateResponse}).
|
||||
*/
|
||||
@GET("games/{gameId}")
|
||||
Call<GameStateResponse> getGameState(@Path("gameId") String gameId);
|
||||
|
||||
/**
|
||||
* Soumet le mouvement d'un joueur pour une partie spécifique.
|
||||
* Le serveur validera si c'est bien le tour de ce joueur.
|
||||
* @param gameId L'identifiant unique de la partie.
|
||||
* @param moveRequest L'objet contenant la direction du mouvement et l'ID du joueur.
|
||||
* @return Le nouvel état du jeu après application du mouvement (ou un message d'erreur).
|
||||
* Soumet un mouvement effectué par un joueur dans une partie spécifique.
|
||||
* Effectue une requête POST vers {@code /api/games/{gameId}/moves}.
|
||||
* Le corps de la requête contient les détails du mouvement (direction et identifiant du joueur).
|
||||
*
|
||||
* @param gameId L'identifiant unique de la partie dans laquelle le mouvement est effectué.
|
||||
* Ce paramètre est inséré dans le chemin de l'URL.
|
||||
* @param moveRequest Un objet {@link MoveRequest} contenant la direction du mouvement
|
||||
* et l'identifiant du joueur effectuant le mouvement.
|
||||
* @return Un objet {@link Call} qui, en cas de succès, encapsule le nouvel état
|
||||
* de la partie après l'application du mouvement ({@link GameStateResponse}).
|
||||
* La réponse peut indiquer si le mouvement était valide, l'état mis à jour du plateau,
|
||||
* les scores, etc.
|
||||
*/
|
||||
@POST("games/{gameId}/moves")
|
||||
Call<GameStateResponse> makeMove(@Path("gameId") String gameId, @Body MoveRequest moveRequest);
|
||||
|
@ -42,53 +42,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/general_section" />
|
||||
|
||||
<LinearLayout
|
||||
style="@style/SectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/high_score_stats_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/high_score_stats" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_games_played_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_games_played" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_games_started_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_games_started" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/win_percentage_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/win_percentage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_play_time_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_play_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_moves_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_moves" />
|
||||
<TextView android:id="@+id/high_score_stats_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/high_score_stats" />
|
||||
<TextView android:id="@+id/total_games_played_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_games_played" />
|
||||
<TextView android:id="@+id/total_games_started_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_games_started" />
|
||||
<TextView android:id="@+id/win_percentage_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/win_percentage" />
|
||||
<TextView android:id="@+id/total_play_time_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_play_time" />
|
||||
<TextView android:id="@+id/total_moves_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_moves" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@ -96,32 +59,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current_game_section" />
|
||||
|
||||
<LinearLayout
|
||||
style="@style/SectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_moves_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current_moves" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_game_time_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current_game_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/merges_this_game"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/merges_this_game_label" />
|
||||
<TextView android:id="@+id/current_moves_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/current_moves" />
|
||||
<TextView android:id="@+id/current_game_time_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/current_game_time" />
|
||||
<TextView android:id="@+id/merges_this_game" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/merges_this_game_label" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@ -129,60 +73,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/single_player_section" />
|
||||
|
||||
<LinearLayout
|
||||
style="@style/SectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/average_game_time_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/average_time_per_game" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/best_winning_time_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/best_winning_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/worst_winning_time_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/worst_winning_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_merges_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_merges" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/highest_tile_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/highest_tile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/number_of_time_objective_reached_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/number_of_time_objective_reached" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/perfect_game_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/perfect_games" />
|
||||
<TextView android:id="@+id/average_game_time_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/average_time_per_game" />
|
||||
<TextView android:id="@+id/best_winning_time_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/best_winning_time" />
|
||||
<TextView android:id="@+id/worst_winning_time_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/worst_winning_time" />
|
||||
<TextView android:id="@+id/total_merges_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_merges" />
|
||||
<TextView android:id="@+id/highest_tile_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/highest_tile" />
|
||||
<TextView android:id="@+id/number_of_time_objective_reached_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/number_of_time_objective_reached" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@ -190,67 +90,23 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_section" />
|
||||
|
||||
<LinearLayout
|
||||
style="@style/SectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView android:id="@+id/multiplayer_games_won_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_games_won" />
|
||||
<TextView android:id="@+id/multiplayer_games_played_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_games_played" />
|
||||
<TextView android:id="@+id/multiplayer_win_rate_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_win_rate" />
|
||||
<TextView android:id="@+id/multiplayer_best_winning_streak_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_best_winning_streak" />
|
||||
<TextView android:id="@+id/multiplayer_average_score_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_average_score" />
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_games_won_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_games_won" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_games_played_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_games_played" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_win_rate_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_win_rate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_best_winning_streak_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_best_winning_streak" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_average_score_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_average_score" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/average_time_per_game_label"
|
||||
android:id="@+id/average_time_per_game_multi_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/average_time_per_game_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_multiplayer_losses_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/total_multiplayer_losses" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiplayer_high_score_label"
|
||||
style="@style/StatLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_high_score" />
|
||||
<TextView android:id="@+id/total_multiplayer_losses_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/total_multiplayer_losses" />
|
||||
<TextView android:id="@+id/multiplayer_high_score_label" style="@style/StatLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/multiplayer_high_score" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Best 2048</string>
|
||||
<string name="name_2048">2048</string>
|
||||
@ -27,15 +28,15 @@
|
||||
<string name="worst_winning_time">Worst Winning Time: %s</string>
|
||||
<string name="total_merges">Total Merges: %d</string>
|
||||
<string name="highest_tile">Highest Tile: %d</string>
|
||||
<string name="number_of_time_objective_reached">Number of time objective reached: %d</string>
|
||||
<string name="perfect_games">Perfect game : %d</string>
|
||||
<string name="multiplayer_games_won">Multiplayer game won : %d</string>
|
||||
<string name="multiplayer_games_played">Multiplayer game played : %d</string>
|
||||
<string name="multiplayer_win_rate">Multiplayer win rate : %s</string>
|
||||
<string name="multiplayer_best_winning_streak">Best winning streak : %d</string>
|
||||
<string name="number_of_time_objective_reached">Times Objective Reached: %d</string>
|
||||
<string name="perfect_games">Perfect Games: %d</string>
|
||||
<string name="multiplayer_games_won">Multiplayer Games Won: %d</string>
|
||||
<string name="multiplayer_games_played">Multiplayer Games Played: %d</string>
|
||||
<string name="multiplayer_win_rate">Multiplayer Win Rate: %s</string>
|
||||
<string name="multiplayer_best_winning_streak">Best Winning Streak: %d</string>
|
||||
<string name="multiplayer_average_score">Multiplayer Average Score: %d</string>
|
||||
<string name="average_time_per_game_label">Average time per game: %s</string>
|
||||
<string name="total_multiplayer_losses">Total Multiplayer losses: %d</string>
|
||||
<string name="average_time_per_game_label">Average Time Per Game: %s</string>
|
||||
<string name="total_multiplayer_losses">Total Multiplayer Losses: %d</string>
|
||||
<string name="multiplayer_high_score">Multiplayer High Score: %d</string>
|
||||
<string name="stats_button_label">Stats</string>
|
||||
<string name="stats_title">Statistics</string>
|
||||
@ -44,13 +45,13 @@
|
||||
<string name="single_player_section">Single Player</string>
|
||||
<string name="multiplayer_section">Multiplayer</string>
|
||||
<string name="back_button_label">Back</string>
|
||||
<string name="you_won_title">You won!</string>
|
||||
<string name="you_won_title">You Won!</string>
|
||||
<string name="you_won_message">Congratulations, you\'ve reached 2048!</string>
|
||||
<string name="keep_playing">Continue</string>
|
||||
<string name="new_game">New Part</string>
|
||||
<string name="game_over_title">Game over!</string>
|
||||
<string name="game_over_message">No move possible.\nFinal score: %d</string>
|
||||
<string name="quit">To leave</string>
|
||||
<string name="keep_playing">Keep Playing</string>
|
||||
<string name="new_game">New Game</string>
|
||||
<string name="game_over_title">Game Over!</string>
|
||||
<string name="game_over_message">No more moves possible.\nFinal Score: %d</string>
|
||||
<string name="quit">Quit</string>
|
||||
<string name="menu_title">Main Menu</string>
|
||||
<string name="menu_option_how_to_play">How to Play</string>
|
||||
<string name="menu_option_settings">Settings</string>
|
||||
@ -60,19 +61,20 @@
|
||||
<string name="how_to_play_instructions">Swipe the screen (Up, Down, Left, Right) to move all the tiles.\n\nWhen two tiles with the same number touch, they merge into one!\n\nReach the 2048 tile to win.\n\nThe game is over if the board is full and no moves are possible.</string>
|
||||
<string name="about_title">About Best 2048</string>
|
||||
<string name="about_message">Version: 1.0 (University Project)\nDeveloped by: La Legion de Muyue\n(Leader: Muyue, Members: 2 others)\n\nBased on the popular game 2048.</string>
|
||||
<string name="about_website_text">Website : legion-muyue.fr</string> <string name="about_website_url">https://legion-muyue.fr</string>
|
||||
<string name="about_website_text">Website: legion-muyue.fr</string>
|
||||
<string name="about_website_url">https://legion-muyue.fr</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_sound_label">Sound</string>
|
||||
<string name="settings_notifications_label">Notifications</string>
|
||||
<string name="settings_permissions_button">Manage Permissions</string>
|
||||
<string name="settings_share_stats_button">Share my Statistics</string>
|
||||
<string name="settings_share_stats_button">Share Statistics</string>
|
||||
<string name="settings_reset_stats_button">Reset Statistics</string>
|
||||
<string name="settings_quit_app_button">Quit Application</string>
|
||||
<string name="settings_close_button">Close</string>
|
||||
<string name="reset_stats_confirm_title">Reset Stats?</string>
|
||||
<string name="reset_stats_confirm_message">Are you sure you want to erase all your saved statistics? This action is irreversible.</string>
|
||||
<string name="share_stats_title">Share my stats via…</string>
|
||||
<string name="share_stats_title">Share stats via…</string>
|
||||
<string name="share_stats_subject">My 2048 Statistics</string>
|
||||
<string name="share_stats_body">Here are my stats on Best 2048:\n- Best Score: %d\n- Highest Tile: %d\n- Games Won: %d / %d\n- Total Time: %s\n- Total Moves: %d</string>
|
||||
<string name="stats_reset_confirmation">Statistics reset.</string>
|
||||
@ -94,29 +96,30 @@
|
||||
<string name="sound_enabled">Sound effects enabled.</string>
|
||||
<string name="sound_disabled">Sound effects disabled.</string>
|
||||
|
||||
<string name="multiplayer_status_searching">Recherche d\'une partie…</string>
|
||||
<string name="multiplayer_status_found">Partie trouvée ! ID: %s…</string> <string name="multiplayer_status_connecting">Connexion au serveur…</string>
|
||||
<string name="multiplayer_status_waiting_state">Connecté. En attente de l\'état du jeu…</string>
|
||||
<string name="multiplayer_status_waiting_opponent">En attente du coup adverse…</string>
|
||||
<string name="multiplayer_status_sending_move">Envoi du mouvement…</string>
|
||||
<string name="multiplayer_status_searching">Searching for a game…</string>
|
||||
<string name="multiplayer_status_found">Game found! ID: %s…</string>
|
||||
<string name="multiplayer_status_connecting">Connecting to server…</string>
|
||||
<string name="multiplayer_status_waiting_state">Connected. Waiting for game state…</string>
|
||||
<string name="multiplayer_status_waiting_opponent">Waiting for opponent\'s move…</string>
|
||||
<string name="multiplayer_status_sending_move">Sending move…</string>
|
||||
|
||||
<string name="multiplayer_turn_yours">À Votre Tour</string>
|
||||
<string name="multiplayer_turn_opponent">Tour Adversaire</string>
|
||||
<string name="multiplayer_turn_yours">Your Turn</string>
|
||||
<string name="multiplayer_turn_opponent">Opponent\'s Turn</string>
|
||||
|
||||
<string name="multiplayer_my_score">Moi :\n%d</string>
|
||||
<string name="multiplayer_opponent_score">Autre :\n%d</string>
|
||||
<string name="error_join_create_game">Impossible de créer ou rejoindre (Code: %d)</string>
|
||||
<string name="error_server_connection">Échec de connexion au serveur.</string>
|
||||
<string name="error_websocket_connection">Erreur de connexion WebSocket.</string>
|
||||
<string name="error_websocket_disconnected">WebSocket déconnecté. Tentative de reconnexion…</string>
|
||||
<string name="error_not_your_turn">Ce n\'est pas votre tour.</string>
|
||||
<string name="server_error_prefix">Erreur Serveur: %s</string>
|
||||
<string name="websocket_connected">Connecté au serveur de jeu !</string>
|
||||
<string name="websocket_closing">Fermeture de la connexion…</string>
|
||||
<string name="websocket_closed">Connexion fermée.</string>
|
||||
<string name="multiplayer_my_score">You:\n%d</string>
|
||||
<string name="multiplayer_opponent_score">Opponent:\n%d</string>
|
||||
<string name="error_join_create_game">Could not create or join game (Code: %d)</string>
|
||||
<string name="error_server_connection">Failed to connect to server.</string>
|
||||
<string name="error_websocket_connection">WebSocket connection error.</string>
|
||||
<string name="error_websocket_disconnected">WebSocket disconnected. Attempting to reconnect…</string>
|
||||
<string name="error_not_your_turn">It\'s not your turn.</string>
|
||||
<string name="server_error_prefix">Server Error: %s</string>
|
||||
<string name="websocket_connected">Connected to game server!</string>
|
||||
<string name="websocket_closing">Closing connection…</string>
|
||||
<string name="websocket_closed">Connection closed.</string>
|
||||
|
||||
<string name="game_over_draw">Égalité !</string>
|
||||
<string name="game_over_won">Vous avez Gagné !</string>
|
||||
<string name="game_over_lost">Vous avez Perdu.</string>
|
||||
<string name="game_over_generic">Partie Terminée !</string>
|
||||
<string name="game_over_draw">It\'s a Draw!</string>
|
||||
<string name="game_over_won">You Won!</string>
|
||||
<string name="game_over_lost">You Lost.</string>
|
||||
<string name="game_over_generic">Game Over!</string>
|
||||
</resources>
|
@ -13,6 +13,7 @@ activity = "1.8.0"
|
||||
constraintlayout = "2.1.4"
|
||||
gridlayout = "1.0.0"
|
||||
retrofit = "2.9.0"
|
||||
workRuntime = "2.10.0"
|
||||
|
||||
[libraries]
|
||||
activity-v190 = { module = "androidx.activity:activity", version.ref = "androidxActivity" }
|
||||
@ -30,6 +31,7 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo
|
||||
gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "loggingInterceptor" }
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user