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 {
|
defaultConfig {
|
||||||
applicationId = "legion.muyue.best2048"
|
applicationId = "legion.muyue.best2048"
|
||||||
minSdk = 33
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 33
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@ -18,7 +18,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@ -46,4 +47,5 @@ dependencies {
|
|||||||
implementation(libs.logging.interceptor)
|
implementation(libs.logging.interceptor)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.work.runtime)
|
||||||
}
|
}
|
@ -31,11 +31,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.Best2048" />
|
android:theme="@style/Theme.Best2048" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".NotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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;
|
package legion.muyue.best2048;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable; // Ajout pour le retour de deserialize
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
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 {
|
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;
|
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;
|
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;
|
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;
|
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;
|
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;
|
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;
|
private boolean gameOver = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructeur pour démarrer une nouvelle partie.
|
* Constructeur par défaut pour démarrer une nouvelle partie.
|
||||||
* Initialise un plateau vide, met le score courant à 0, réinitialise les états
|
* Initialise un plateau vide, met le score à zéro, et ajoute deux tuiles initiales aléatoires.
|
||||||
* de victoire/défaite, et ajoute deux tuiles initiales aléatoirement.
|
|
||||||
*/
|
*/
|
||||||
public Game() {
|
public Game() {
|
||||||
this.randomNumberGenerator = new Random();
|
this.randomNumberGenerator = new Random();
|
||||||
@ -44,58 +67,54 @@ public class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructeur pour restaurer une partie à partir d'un état sauvegardé.
|
* Constructeur pour créer une instance de jeu à partir d'un état existant.
|
||||||
* Le meilleur score (`highestScore`) doit être défini séparément via {@link #setHighestScore(int)}.
|
* Utile pour charger une partie sauvegardée ou pour les tests.
|
||||||
* Recalcule les états `gameWon` et `gameOver` en fonction du plateau et du score fournis.
|
* Vérifie si les conditions de victoire ou de fin de partie sont déjà remplies.
|
||||||
* Lance une exception si le plateau fourni n'a pas les bonnes dimensions.
|
|
||||||
*
|
*
|
||||||
* @param board Le plateau de jeu restauré. Doit être de dimensions BOARD_SIZE x BOARD_SIZE.
|
* @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 courant restauré.
|
* @param score Le score associé à l'état du plateau fourni.
|
||||||
* @throws IllegalArgumentException si les dimensions du plateau fourni sont incorrectes.
|
* @throws IllegalArgumentException si le plateau fourni est null ou n'a pas les dimensions correctes.
|
||||||
*/
|
*/
|
||||||
public Game(@NonNull int[][] board, int score) {
|
public Game(@NonNull int[][] board, int score) {
|
||||||
// Validation des dimensions du plateau
|
|
||||||
if (board == null || board.length != BOARD_SIZE || board[0].length != BOARD_SIZE) {
|
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 + ").");
|
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.
|
// Crée une copie défensive du tableau pour éviter les modifications externes
|
||||||
// Pour la restauration, on suppose que le tableau fourni est destiné à cet usage unique.
|
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.currentScore = score;
|
||||||
this.randomNumberGenerator = new Random();
|
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();
|
checkWinCondition();
|
||||||
// Vérifier si la partie chargée est déjà terminée
|
checkGameOverCondition();
|
||||||
checkGameOverCondition(); // Important si on charge une partie déjà terminée
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Getters / Setters ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne la valeur de la tuile aux coordonnées spécifiées (ligne, colonne).
|
* Récupère la valeur de la tuile à la position spécifiée.
|
||||||
* Les indices sont basés sur 0.
|
|
||||||
*
|
*
|
||||||
* @param row Ligne de la cellule (0 à BOARD_SIZE-1).
|
* @param row L'indice de la ligne (0 à BOARD_SIZE - 1).
|
||||||
* @param column Colonne de la cellule (0 à BOARD_SIZE-1).
|
* @param column L'indice de la colonne (0 à BOARD_SIZE - 1).
|
||||||
* @return La valeur de la tuile, ou 0 si la cellule est vide ou les coordonnées sont invalides.
|
* @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) {
|
public int getCellValue(int row, int column) {
|
||||||
if (isIndexValid(row, column)) {
|
if (isIndexValid(row, column)) {
|
||||||
return this.board[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 si l'index est hors limites
|
||||||
return 0; // Retourne 0 pour indice invalide comme convention
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Définit la valeur d'une tuile aux coordonnées spécifiées.
|
* Définit la valeur d'une cellule spécifique sur le plateau.
|
||||||
* Utilisé principalement en interne ou pour les tests. Ne fait rien si
|
* Principalement destiné aux tests unitaires pour configurer des scénarios spécifiques.
|
||||||
* les coordonnées (ligne, colonne) sont en dehors des limites du plateau.
|
* Utiliser avec prudence car cela modifie directement l'état interne du jeu.
|
||||||
*
|
*
|
||||||
* @param row Ligne de la cellule (0 à BOARD_SIZE-1).
|
* @param row L'indice de la ligne (0 à BOARD_SIZE - 1).
|
||||||
* @param col Colonne de la cellule (0 à BOARD_SIZE-1).
|
* @param col L'indice de la colonne (0 à BOARD_SIZE - 1).
|
||||||
* @param value Nouvelle valeur entière pour la tuile.
|
* @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) {
|
void setCellValue(int row, int col, int value) {
|
||||||
if (isIndexValid(row, col)) {
|
if (isIndexValid(row, col)) {
|
||||||
this.board[row][col] = value;
|
this.board[row][col] = value;
|
||||||
@ -103,135 +122,121 @@ public class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne le score actuel de la partie en cours.
|
* Récupère le score actuel de la partie.
|
||||||
* Le score augmente lors de la fusion de tuiles.
|
|
||||||
*
|
*
|
||||||
* @return Le score entier actuel.
|
* @return Le score actuel.
|
||||||
*/
|
*/
|
||||||
public int getCurrentScore() {
|
public int getCurrentScore() {
|
||||||
return currentScore;
|
return currentScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne le meilleur score connu par cette instance de jeu.
|
* Récupère le meilleur score enregistré (peut nécessiter une gestion externe).
|
||||||
* Cette valeur est généralement chargée depuis une sauvegarde externe et mise à jour via {@link #setHighestScore(int)}.
|
|
||||||
*
|
*
|
||||||
* @return Le meilleur score entier connu.
|
* @return Le meilleur score connu.
|
||||||
*/
|
*/
|
||||||
public int getHighestScore() {
|
public int getHighestScore() {
|
||||||
return highestScore;
|
return highestScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour la valeur du meilleur score stockée dans cet objet Game.
|
* Définit le meilleur score. Utile pour charger un meilleur score sauvegardé.
|
||||||
* Cette méthode est typiquement appelée par la classe gérant la persistance
|
|
||||||
* (par exemple, MainActivity) après avoir chargé le meilleur score global.
|
|
||||||
*
|
*
|
||||||
* @param highScore Le nouveau meilleur score global à stocker dans cette instance.
|
* @param highScore Le meilleur score à définir.
|
||||||
*/
|
*/
|
||||||
public void setHighestScore(int highScore) {
|
public void setHighestScore(int highScore) {
|
||||||
this.highestScore = 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() {
|
public boolean isGameWon() {
|
||||||
return gameWon;
|
return gameWon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si la partie est terminée.
|
* Vérifie si la condition de fin de partie (aucun mouvement possible) a été remplie.
|
||||||
* La partie est terminée si aucune case n'est vide ET aucun mouvement ou fusion n'est possible
|
|
||||||
* dans aucune des quatre directions.
|
|
||||||
*
|
*
|
||||||
* @return true si la partie est terminée (game over), false sinon.
|
* @return {@code true} si le jeu est terminé, {@code false} sinon.
|
||||||
*/
|
*/
|
||||||
public boolean isGameOver() {
|
public boolean isGameOver() {
|
||||||
return gameOver;
|
return gameOver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour l'indicateur interne de victoire du jeu.
|
* Définit l'état de victoire du jeu. Méthode privée utilisée en interne.
|
||||||
* Utilisé après une fusion ou lors de la restauration d'une partie.
|
* @param won Nouvel état de victoire.
|
||||||
*
|
|
||||||
* @param won true si la condition de victoire est atteinte, false sinon.
|
|
||||||
*/
|
*/
|
||||||
private void setGameWon(boolean won) {
|
private void setGameWon(boolean won) {
|
||||||
this.gameWon = won;
|
this.gameWon = won;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour l'indicateur interne de fin de partie (game over).
|
* Définit l'état de fin de partie du jeu. Méthode privée utilisée en interne.
|
||||||
* Utilisé après une tentative de mouvement infructueuse ou lors de la restauration.
|
* @param over Nouvel état de fin de partie.
|
||||||
*
|
|
||||||
* @param over true si la condition de fin de partie est atteinte, false sinon.
|
|
||||||
*/
|
*/
|
||||||
private void setGameOver(boolean over) {
|
private void setGameOver(boolean over) {
|
||||||
this.gameOver = over;
|
this.gameOver = over;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne une copie profonde (deep copy) du plateau de jeu actuel.
|
* Retourne une copie du plateau de jeu actuel.
|
||||||
* Ceci est utile pour obtenir l'état du plateau sans risquer de le modifier
|
* Utiliser cette méthode pour obtenir l'état du plateau sans risquer de
|
||||||
* accidentellement de l'extérieur, ou pour la sérialisation.
|
* 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
|
@NonNull
|
||||||
public int[][] getBoard() {
|
public int[][] getBoard() {
|
||||||
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
|
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
|
||||||
for (int i = 0; i < BOARD_SIZE; i++) {
|
for (int i = 0; i < BOARD_SIZE; i++) {
|
||||||
// System.arraycopy est efficace pour copier des tableaux primitifs
|
// Utilise arraycopy pour une copie efficace de chaque ligne
|
||||||
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
|
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
|
||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Ré)Initialise le plateau de jeu pour une nouvelle partie.
|
* Initialise ou réinitialise le plateau pour une nouvelle partie.
|
||||||
* Crée une nouvelle matrice vide (remplie de 0), réinitialise le score courant
|
* Met toutes les cellules à 0, réinitialise le score et les indicateurs
|
||||||
* et les indicateurs `gameWon`/`gameOver`, puis appelle {@link #addNewTile()}
|
* de victoire/fin de partie, puis ajoute deux nouvelles tuiles.
|
||||||
* deux fois pour placer les deux premières tuiles aléatoires.
|
|
||||||
*/
|
*/
|
||||||
private void initializeNewBoard() {
|
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.currentScore = 0;
|
||||||
this.gameWon = false;
|
this.gameWon = false;
|
||||||
this.gameOver = false;
|
this.gameOver = false;
|
||||||
// Ajoute les deux premières tuiles requises pour démarrer une partie
|
addNewTile(); // Ajoute la première tuile
|
||||||
addNewTile();
|
addNewTile(); // Ajoute la deuxième tuile
|
||||||
addNewTile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logique du Jeu ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon les probabilités définies
|
* Ajoute une nouvelle tuile aléatoire (2, 4, 8, ...) sur une case vide aléatoire du plateau.
|
||||||
* dans {@link #generateRandomTileValue()}) sur une case vide choisie aléatoirement.
|
* Si aucune case n'est vide, cette méthode n'a aucun effet.
|
||||||
* Si le plateau est plein (aucune case vide), cette méthode ne fait rien.
|
* La valeur de la nouvelle tuile est déterminée par {@link #generateRandomTileValue()}.
|
||||||
*/
|
*/
|
||||||
public void addNewTile() {
|
public void addNewTile() {
|
||||||
List<int[]> emptyCells = findEmptyCells();
|
List<int[]> emptyCells = findEmptyCells();
|
||||||
if (!emptyCells.isEmpty()) {
|
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()));
|
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
|
||||||
|
// Génère la valeur de la nouvelle tuile (2, 4, 8, ...)
|
||||||
int value = generateRandomTileValue();
|
int value = generateRandomTileValue();
|
||||||
// Place la nouvelle tuile sur le plateau logique
|
// Place la nouvelle tuile sur le plateau
|
||||||
// Utilise setCellValue pour la cohérence, même si l'accès direct serait possible ici
|
|
||||||
setCellValue(randomCell[0], randomCell[1], value);
|
setCellValue(randomCell[0], randomCell[1], value);
|
||||||
}
|
}
|
||||||
// Si emptyCells est vide, le plateau est plein, on ne peut rien ajouter.
|
// Après ajout, revérifie si le jeu est terminé (au cas où l'ajout bloquerait tout)
|
||||||
// La condition de Game Over sera vérifiée après la tentative de mouvement.
|
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.
|
* @return Une liste de tableaux d'entiers {@code [row, col]}, où chaque tableau représente
|
||||||
* Retourne une liste vide si le plateau est plein.
|
* 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
|
@NonNull
|
||||||
private List<int[]> findEmptyCells() {
|
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
|
* Génère la valeur pour une nouvelle tuile ajoutée au plateau.
|
||||||
* de probabilités prédéfinie. Les probabilités sont ajustées pour rendre l'apparition
|
* Cette implémentation spécifique a des probabilités pour 2, 4, 8, 16, 32, 64, 128, 256.
|
||||||
* des tuiles de faible valeur plus fréquente.
|
* ~85.4% de chance pour 2, ~12% pour 4, ~2% pour 8, ~0.5% pour 16, etc.
|
||||||
*
|
*
|
||||||
* Probabilités actuelles (approximatives) :
|
* @return La valeur de la nouvelle tuile (2, 4, 8, ... , 256).
|
||||||
* - 2: 85.40%
|
|
||||||
* - 4: 12.00%
|
|
||||||
* - 8: 2.00%
|
|
||||||
* - 16: 0.50%
|
|
||||||
* - 32: 0.05%
|
|
||||||
* - 64: 0.03%
|
|
||||||
* - 128: 0.01%
|
|
||||||
* - 256: 0.01%
|
|
||||||
*
|
|
||||||
* @return La valeur entière (2, 4, 8, ...) de la tuile générée.
|
|
||||||
*/
|
*/
|
||||||
private int generateRandomTileValue() {
|
private int generateRandomTileValue() {
|
||||||
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour une granularité fine des pourcentages
|
// Note: Les probabilités ici diffèrent du 2048 standard (90% 2, 10% 4).
|
||||||
|
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour les pourcentages
|
||||||
// Les seuils définissent les probabilités cumulées
|
if (randomValue < 8540) return 2; // ~85.4%
|
||||||
if (randomValue < 8540) return 2; // 0 <= randomValue < 8540 (85.40%)
|
if (randomValue < 9740) return 4; // ~12.0% (9740 - 8540)
|
||||||
if (randomValue < 9740) return 4; // 8540 <= randomValue < 9740 (12.00%)
|
if (randomValue < 9940) return 8; // ~ 2.0% (9940 - 9740)
|
||||||
if (randomValue < 9940) return 8; // 9740 <= randomValue < 9940 (2.00%)
|
if (randomValue < 9990) return 16; // ~ 0.5% (9990 - 9940)
|
||||||
if (randomValue < 9990) return 16; // 9940 <= randomValue < 9990 (0.50%)
|
if (randomValue < 9995) return 32; // ~0.05% (9995 - 9990)
|
||||||
if (randomValue < 9995) return 32; // 9990 <= randomValue < 9995 (0.05%)
|
if (randomValue < 9998) return 64; // ~0.03% (9998 - 9995)
|
||||||
if (randomValue < 9998) return 64; // 9995 <= randomValue < 9998 (0.03%)
|
if (randomValue < 9999) return 128;// ~0.01% (9999 - 9998)
|
||||||
if (randomValue < 9999) return 128; // 9998 <= randomValue < 9999 (0.01%)
|
return 256; // ~0.01%
|
||||||
return 256; // 9999 <= randomValue < 10000 (0.01%)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tente de déplacer et fusionner les tuiles vers le HAUT.
|
* Tente d'effectuer un mouvement vers le haut.
|
||||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
* 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() {
|
public boolean pushUp() {
|
||||||
return processMove(MoveDirection.UP);
|
return processMove(MoveDirection.UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tente de déplacer et fusionner les tuiles vers le BAS.
|
* Tente d'effectuer un mouvement vers le bas.
|
||||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
* 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() {
|
public boolean pushDown() {
|
||||||
return processMove(MoveDirection.DOWN);
|
return processMove(MoveDirection.DOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
|
* Tente d'effectuer un mouvement vers la gauche.
|
||||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
* 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() {
|
public boolean pushLeft() {
|
||||||
return processMove(MoveDirection.LEFT);
|
return processMove(MoveDirection.LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tente de déplacer et fusionner les tuiles vers la DROITE.
|
* Tente d'effectuer un mouvement vers la droite.
|
||||||
* Met à jour le plateau, le score interne, et vérifie les états de victoire/défaite.
|
* 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() {
|
public boolean pushRight() {
|
||||||
return processMove(MoveDirection.RIGHT);
|
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 }
|
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.
|
* Traite un mouvement dans la direction spécifiée.
|
||||||
* C'est le cœur de la logique de déplacement du jeu. Elle parcourt le plateau, déplace
|
* Parcourt le plateau ligne par ligne ou colonne par colonne, déplace les tuiles,
|
||||||
* les tuiles jusqu'au bout dans la direction demandée, puis gère les fusions éventuelles.
|
* 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}).
|
* @param direction La direction {@link MoveDirection} du mouvement à traiter.
|
||||||
* @return true si le plateau a été modifié (au moins un déplacement ou une fusion), false sinon.
|
* @return {@code true} si au moins une tuile a bougé ou fusionné, {@code false} sinon.
|
||||||
*/
|
*/
|
||||||
private boolean processMove(MoveDirection direction) {
|
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++) {
|
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
|
// Tableau pour suivre si une cellule de la ligne/colonne cible a déjà fusionné dans ce mouvement
|
||||||
// pendant CE mouvement, pour éviter les fusions en chaîne (ex: 2-2-4 -> 4-4 -> 8 en un seul swipe)
|
|
||||||
boolean[] hasMerged = new boolean[BOARD_SIZE];
|
boolean[] hasMerged = new boolean[BOARD_SIZE];
|
||||||
|
|
||||||
// Itérer sur l'axe du mouvement. Le sens de parcours est crucial :
|
// Détermine l'ordre de parcours des cellules dans la ligne/colonne
|
||||||
// - Pour UP/LEFT: du début vers la fin (index 1 à N-1) car les tuiles fusionnent vers les petits indices.
|
// Pour HAUT et GAUCHE : on part du début (index 1) vers la fin
|
||||||
// - Pour DOWN/RIGHT: de la fin vers le début (index N-2 à 0) car les tuiles fusionnent vers les grands indices.
|
// 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 start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
|
||||||
int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
|
int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
|
||||||
int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;
|
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) {
|
for (int j = start; j != end; j += step) {
|
||||||
int row, col;
|
int row, col; // Coordonnées de la cellule actuelle (j)
|
||||||
// Déterminer les coordonnées (row, col) de la tuile courante basée sur l'axe i et l'index j
|
|
||||||
if (direction == MoveDirection.UP || direction == MoveDirection.DOWN) {
|
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 {
|
} 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) {
|
if (currentValue != 0) {
|
||||||
int currentRow = row;
|
int currentRow = row; // Position initiale de la tuile
|
||||||
int currentCol = col;
|
int currentCol = col;
|
||||||
int targetRow = currentRow; // Position cible initiale
|
int targetRow = currentRow; // Position cible après déplacement (sans fusion)
|
||||||
int targetCol = currentCol;
|
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 nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||||
int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 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) {
|
while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
|
||||||
targetRow = nextRow;
|
targetRow = nextRow;
|
||||||
targetCol = nextCol;
|
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);
|
nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
|
||||||
nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 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)
|
// nextRow/nextCol contient maintenant la première case non vide rencontrée ou une case invalide
|
||||||
// La case à vérifier pour fusion est `nextRow`, `nextCol` (calculée juste avant ou après la sortie du while)
|
int mergeTargetRow = nextRow; // Case potentielle pour la fusion
|
||||||
int mergeTargetRow = nextRow;
|
|
||||||
int mergeTargetCol = nextCol;
|
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;
|
int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;
|
||||||
|
|
||||||
boolean merged = false;
|
boolean merged = false; // Indicateur si une fusion a eu lieu pour cette tuile
|
||||||
// Vérifier si la case de fusion potentielle est valide, a la même valeur, et n'a pas déjà fusionné
|
// Vérifie si une fusion est possible
|
||||||
if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
|
if (isIndexValid(mergeTargetRow, mergeTargetCol) && // La case cible est valide
|
||||||
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
|
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue && // Elle a la même valeur
|
||||||
!hasMerged[mergeIndex]) // Crucial: empêche double fusion
|
!hasMerged[mergeIndex]) // Elle n'a pas déjà fusionné ce tour-ci
|
||||||
{
|
{
|
||||||
// Fusion !
|
// --- Fusion ---
|
||||||
int newValue = currentValue * 2;
|
int newValue = currentValue * 2; // Nouvelle valeur après fusion
|
||||||
setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible de la fusion
|
setCellValue(mergeTargetRow, mergeTargetCol, newValue); // Met à jour la case cible
|
||||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine de la tuile qui a bougé ET fusionné
|
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
||||||
currentScore += newValue; // Ajoute la *nouvelle* valeur au score
|
currentScore += newValue; // Augmente le score
|
||||||
hasMerged[mergeIndex] = true; // Marque la case cible comme ayant fusionné
|
hasMerged[mergeIndex] = true; // Marque la case cible comme ayant fusionné
|
||||||
boardChanged = true; // Le plateau a changé
|
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
|
// Vérifie la condition de victoire après la fusion
|
||||||
if (newValue >= 2048) {
|
if (newValue >= 2048) { // Utiliser une constante WINNING_TILE serait mieux
|
||||||
setGameWon(true);
|
setGameWon(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Déplacement final (si pas de fusion OU si la tuile a bougé avant de potentiellement fusionner)
|
// Si aucune fusion n'a eu lieu mais que la tuile doit bouger
|
||||||
// Si la tuile n'a pas fusionné (elle reste où elle est ou se déplace seulement)
|
|
||||||
// ET que sa position cible (targetRow, targetCol) est différente de sa position initiale
|
|
||||||
if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
|
if (!merged && (targetRow != currentRow || targetCol != currentCol)) {
|
||||||
|
// --- Déplacement simple ---
|
||||||
setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
|
setCellValue(targetRow, targetCol, currentValue); // Déplace la valeur vers la case cible
|
||||||
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
setCellValue(currentRow, currentCol, 0); // Vide la case d'origine
|
||||||
boardChanged = true; // Le plateau a changé
|
boardChanged = true; // Le plateau a changé
|
||||||
}
|
}
|
||||||
} // Fin if (currentValue != 0)
|
} // Fin if (currentValue != 0)
|
||||||
} // Fin boucle interne (j)
|
} // Fin boucle for interne (j)
|
||||||
} // Fin boucle externe (i)
|
} // Fin boucle for externe (i)
|
||||||
|
|
||||||
// Après avoir traité toutes les lignes/colonnes, vérifier l'état global
|
// Après avoir traité toutes les lignes/colonnes, revérifie les conditions
|
||||||
// Note: checkWinCondition est déjà appelé lors d'une fusion >= 2048, mais on le refait ici par sécurité.
|
// (la condition de victoire peut avoir été atteinte pendant la fusion)
|
||||||
checkWinCondition(); // Vérifie si 2048 a été atteint (peut être déjà fait lors d'une fusion)
|
checkWinCondition();
|
||||||
checkGameOverCondition(); // Vérifie si plus aucun mouvement n'est possible
|
// 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;
|
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 row L'indice de la ligne à vérifier.
|
||||||
* @param col L'indice de colonne à vérifier.
|
* @param col L'indice de la colonne à vérifier.
|
||||||
* @return true si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, false sinon.
|
* @return {@code true} si 0 <= row < BOARD_SIZE et 0 <= col < BOARD_SIZE, {@code false} sinon.
|
||||||
*/
|
*/
|
||||||
private boolean isIndexValid(int row, int col) {
|
private boolean isIndexValid(int row, int col) {
|
||||||
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
|
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.
|
* Fournit une représentation textuelle simple de l'état du jeu (plateau + score).
|
||||||
* Le format actuel est simple : toutes les valeurs du plateau séparées par des virgules,
|
* Le format est une chaîne de valeurs séparées par des virgules :
|
||||||
* suivies par le score courant, également séparé par une virgule.
|
* toutes les valeurs du plateau (ligne par ligne), suivies du score actuel.
|
||||||
* Exemple pour un plateau 2x2 : "2,0,4,8,12" (où 12 est le score).
|
* Utilisé pour la sérialisation simple via {@link #deserialize(String)}.
|
||||||
* <p>
|
|
||||||
* NOTE : Ce format est simple mais peut être fragile si la structure du jeu
|
|
||||||
* ou les données à sauvegarder évoluent. Un format comme JSON pourrait offrir plus de flexibilité.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @return Une chaîne non nulle représentant l'état sérialisé du jeu.
|
* @return Une chaîne représentant l'état du jeu. Ne sera jamais null.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@ -456,149 +449,139 @@ public class Game {
|
|||||||
sb.append(board[row][col]).append(",");
|
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);
|
sb.append(currentScore);
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée (désérialise) un objet {@code Game} à partir de sa représentation sous forme de chaîne,
|
* Crée une instance de {@link Game} à partir d'une chaîne sérialisée.
|
||||||
* telle que générée par {@link #toString()}.
|
* La chaîne doit correspondre au format généré par {@link #toString()}.
|
||||||
* Le meilleur score (`highestScore`) n'est pas inclus dans la chaîne et doit être
|
|
||||||
* défini séparément sur l'objet retourné via {@link #setHighestScore(int)}.
|
|
||||||
* <p>
|
|
||||||
* NOTE : Cette méthode dépend du format spécifique généré par {@code toString()}.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @param serializedState La chaîne représentant l'état du jeu (plateau + score courant).
|
* @param serializedState La chaîne contenant l'état du jeu sérialisé. Peut être null ou vide.
|
||||||
* @return Une nouvelle instance de {@code Game} si la désérialisation réussit,
|
* @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, ou a un format incorrect
|
* ou {@code null} si la chaîne est invalide, vide, null, ou ne correspond pas au format attendu.
|
||||||
* (mauvais nombre d'éléments, valeur non entière).
|
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Game deserialize(@Nullable String serializedState) {
|
public static Game deserialize(@Nullable String serializedState) {
|
||||||
if (serializedState == null || serializedState.isEmpty()) {
|
if (serializedState == null || serializedState.isEmpty()) {
|
||||||
return null;
|
return null; // Chaîne vide ou nulle
|
||||||
}
|
}
|
||||||
String[] values = serializedState.split(",");
|
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)) {
|
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[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
|
||||||
int index = 0;
|
int index = 0;
|
||||||
try {
|
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 row = 0; row < BOARD_SIZE; row++) {
|
||||||
for (int col = 0; col < BOARD_SIZE; col++) {
|
for (int col = 0; col < BOARD_SIZE; col++) {
|
||||||
newBoard[row][col] = Integer.parseInt(values[index++]);
|
newBoard[row][col] = Integer.parseInt(values[index++]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Le dernier élément est le score
|
// Récupère le score
|
||||||
int score = Integer.parseInt(values[index]);
|
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é
|
||||||
// Crée une nouvelle instance de Game avec les données désérialisées
|
|
||||||
// Le constructeur recalculera gameWon et gameOver.
|
|
||||||
return new Game(newBoard, score);
|
return new Game(newBoard, score);
|
||||||
|
|
||||||
} catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
|
} catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
|
||||||
// En cas d'erreur de format (valeur non entière, problème d'indice, ou dimension plateau incorrecte dans constructeur)
|
return null; // Échec de la désérialisation
|
||||||
// Log l'erreur pourrait être utile pour le débogage
|
|
||||||
System.err.println("Erreur lors de la désérialisation de l'état du jeu: " + e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si la condition de victoire (au moins une tuile avec une valeur >= 2048)
|
* Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
|
||||||
* est actuellement atteinte sur le plateau. Met à jour l'état interne `gameWon`.
|
* Met à jour le flag {@link #gameWon} si nécessaire.
|
||||||
* Cette vérification s'arrête dès qu'une tuile gagnante est trouvée ou si le jeu
|
* Ne fait rien si le jeu est déjà marqué comme gagné.
|
||||||
* est déjà marqué comme gagné.
|
|
||||||
*/
|
*/
|
||||||
private void checkWinCondition() {
|
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) {
|
if (gameWon) {
|
||||||
return;
|
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 r = 0; r < BOARD_SIZE; r++) {
|
||||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||||
if (getCellValue(r, c) >= 2048) {
|
// Utiliser getCellValue pour la cohérence (bien que l'accès direct soit possible ici)
|
||||||
setGameWon(true); // Met à jour l'état interne
|
if (getCellValue(r, c) >= 2048) { // Utiliser une constante WINNING_TILE = 2048
|
||||||
return; // Sort dès qu'une condition de victoire est trouvée
|
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.
|
* Vérifie si la condition de fin de partie est atteinte (plus aucun mouvement possible).
|
||||||
* Cela se produit si le plateau est plein (pas de case vide) ET si aucune
|
* Un jeu est terminé s'il n'y a plus de cellules vides ET aucune paire de tuiles
|
||||||
* fusion n'est possible entre des tuiles adjacentes (horizontalement ou verticalement).
|
* adjacentes identiques (horizontalement ou verticalement).
|
||||||
* Met à jour l'état interne `gameOver`.
|
* Met à jour le flag {@link #gameOver} si nécessaire.
|
||||||
|
* Ne fait rien si le jeu est déjà marqué comme terminé.
|
||||||
*/
|
*/
|
||||||
private void checkGameOverCondition() {
|
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) {
|
if (gameOver) {
|
||||||
return;
|
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()) {
|
if (hasEmptyCell()) {
|
||||||
setGameOver(false);
|
setGameOver(false); // Assure que le flag est bien à false
|
||||||
return;
|
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 r = 0; r < BOARD_SIZE; r++) {
|
||||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||||
int current = getCellValue(r, 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) {
|
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) {
|
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) {
|
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) {
|
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);
|
setGameOver(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie rapidement si le plateau contient au moins une case vide (valeur 0).
|
* Vérifie rapidement s'il existe au moins une cellule vide sur le plateau.
|
||||||
* Utilisé principalement par {@link #checkGameOverCondition()}.
|
|
||||||
*
|
*
|
||||||
* @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() {
|
private boolean hasEmptyCell() {
|
||||||
for (int r = 0; r < BOARD_SIZE; r++) {
|
for (int r = 0; r < BOARD_SIZE; r++) {
|
||||||
for (int c = 0; c < BOARD_SIZE; c++) {
|
for (int c = 0; c < BOARD_SIZE; c++) {
|
||||||
if (getCellValue(r, c) == 0) {
|
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)
|
* Calcule et retourne la valeur de la plus haute tuile actuellement sur le plateau.
|
||||||
* actuellement présente sur le plateau de jeu.
|
|
||||||
*
|
*
|
||||||
* @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() {
|
public int getHighestTileValue() {
|
||||||
int maxTile = 0;
|
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;
|
package legion.muyue.best2048;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -11,122 +5,130 @@ import android.content.Context;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import java.util.concurrent.TimeUnit;
|
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 {
|
public class GameStats {
|
||||||
|
|
||||||
// --- Constantes pour SharedPreferences ---
|
// --- Constantes pour SharedPreferences ---
|
||||||
|
/** Nom du fichier de préférences partagées utilisé pour stocker les statistiques. */
|
||||||
/** Nom du fichier de préférences partagées utilisé pour stocker les statistiques et l'état du jeu. */
|
|
||||||
private static final String PREFS_NAME = "Best2048_Prefs";
|
private static final String PREFS_NAME = "Best2048_Prefs";
|
||||||
/**
|
/** Clé pour stocker le meilleur score global (principalement solo). */
|
||||||
* Clé pour le meilleur score global.
|
|
||||||
* NOTE : Cette clé est également utilisée directement par MainActivity/Game pour la sauvegarde/chargement
|
|
||||||
* de l'état du jeu. La synchronisation est gérée entre les classes.
|
|
||||||
*/
|
|
||||||
private static final String HIGH_SCORE_KEY = "high_score";
|
private static final String HIGH_SCORE_KEY = "high_score";
|
||||||
|
// Clés pour les statistiques Solo
|
||||||
// Clés spécifiques aux statistiques persistantes
|
/** Clé pour le nombre total de parties solo terminées (victoire ou défaite). */
|
||||||
private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed";
|
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";
|
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";
|
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";
|
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";
|
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";
|
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_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";
|
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";
|
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
|
||||||
|
// Clés pour les statistiques Multijoueur (persistées)
|
||||||
// Clés pour les statistiques multijoueur (fonctionnalité future)
|
/** Clé pour le nombre total de parties multijoueur gagnées. */
|
||||||
private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon";
|
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";
|
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";
|
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";
|
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";
|
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";
|
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";
|
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore";
|
||||||
|
|
||||||
|
// --- Champs Solo & Général (persistés via SharedPreferences) ---
|
||||||
// --- Champs de Statistiques ---
|
/** Nombre total de parties solo terminées (victoires + défaites). Persisté. */
|
||||||
|
|
||||||
// Générales & Solo
|
|
||||||
/** Nombre total de parties terminées (victoire ou défaite). */
|
|
||||||
private int totalGamesPlayed;
|
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;
|
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;
|
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;
|
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;
|
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;
|
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 numberOfTimesObjectiveReached;
|
||||||
// private int perfectGames; // Champ supprimé car concept non défini
|
/** Meilleur temps (le plus court) pour atteindre l'objectif en solo (en millisecondes). Persisté. */
|
||||||
/** Meilleur temps (le plus court, en ms) pour atteindre l'objectif (>= 2048). */
|
|
||||||
private long bestWinningTimeMs;
|
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;
|
private long worstWinningTimeMs;
|
||||||
/**
|
/** Meilleur score global atteint (principalement en mode solo). Persisté. */
|
||||||
* Meilleur score global atteint. Persisté via HIGH_SCORE_KEY.
|
|
||||||
* Synchronisé avec MainActivity/Game lors du chargement/sauvegarde.
|
|
||||||
*/
|
|
||||||
private int overallHighScore;
|
private int overallHighScore;
|
||||||
|
|
||||||
// Partie en cours (non persistées telles quelles, utilisées pour calculs en temps réel)
|
// --- Champs Partie en cours (Solo - transitoires, non persistés) ---
|
||||||
/** Nombre de mouvements effectués dans la partie actuelle. */
|
/** Nombre de mouvements effectués dans la partie solo actuelle. Non persisté. */
|
||||||
private int currentMoves;
|
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;
|
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;
|
private int mergesThisGame;
|
||||||
|
|
||||||
// Multijoueur (placeholders pour fonctionnalité future)
|
// --- Champs Multijoueur (persistés via SharedPreferences) ---
|
||||||
/** Nombre de parties multijoueur gagnées. */
|
/** Nombre total de parties multijoueur gagnées. Persisté. */
|
||||||
private int multiplayerGamesWon;
|
private int multiplayerGamesWon;
|
||||||
/** Nombre de parties multijoueur jouées (terminées). */
|
/** Nombre total de parties multijoueur jouées (terminées). Persisté. */
|
||||||
private int multiplayerGamesPlayed;
|
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;
|
private int multiplayerBestWinningStreak;
|
||||||
/** Score total accumulé dans toutes les parties multijoueur. */
|
/** Score total cumulé sur toutes les parties multijoueur. Persisté. */
|
||||||
private long multiplayerTotalScore;
|
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;
|
private long multiplayerTotalTimeMs;
|
||||||
/** Nombre total de défaites en multijoueur. */
|
/** Nombre total de parties multijoueur perdues. Persisté. */
|
||||||
private int totalMultiplayerLosses;
|
private int totalMultiplayerLosses;
|
||||||
/** Meilleur score atteint dans une seule partie multijoueur. */
|
/** Meilleur score obtenu dans une seule partie multijoueur. Persisté. */
|
||||||
private int multiplayerHighestScore;
|
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;
|
private final Context context;
|
||||||
|
|
||||||
|
// --- Constructeur & Persistance ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructeur de GameStats.
|
* Construit une nouvelle instance de GameStats.
|
||||||
* Initialise l'objet et charge immédiatement les statistiques persistantes
|
* Charge immédiatement les statistiques persistées depuis les SharedPreferences.
|
||||||
* depuis les SharedPreferences via {@link #loadStats()}.
|
|
||||||
*
|
*
|
||||||
* @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) {
|
public GameStats(Context context) {
|
||||||
this.context = context.getApplicationContext(); // Important: utiliser le contexte applicatif
|
this.context = context.getApplicationContext(); // Utilise le contexte d'application pour éviter les fuites de mémoire
|
||||||
loadStats(); // Charge les statistiques dès l'initialisation
|
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}.
|
* Charge toutes les statistiques persistées 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.).
|
* 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).
|
||||||
* Appelé automatiquement par le constructeur.
|
|
||||||
*/
|
*/
|
||||||
public void loadStats() {
|
public void loadStats() {
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
|
||||||
// Charge le high score (clé partagée)
|
|
||||||
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
|
||||||
|
|
||||||
// Charge les statistiques générales et solo
|
|
||||||
totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0);
|
totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0);
|
||||||
totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0);
|
totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0);
|
||||||
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
|
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
|
||||||
@ -134,11 +136,9 @@ public class GameStats {
|
|||||||
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
|
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
|
||||||
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
|
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
|
||||||
numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 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);
|
||||||
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); // 0 est ok pour le pire temps
|
||||||
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0L);
|
// Chargement des stats Multi
|
||||||
|
|
||||||
// Charge les statistiques multijoueur (placeholders)
|
|
||||||
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
|
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
|
||||||
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
|
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
|
||||||
multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 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.
|
* Sauvegarde l'état actuel de toutes les statistiques persistables dans le fichier SharedPreferences.
|
||||||
* Utilise `apply()` pour une sauvegarde asynchrone et non bloquante.
|
* Utilise {@link SharedPreferences.Editor#apply()} pour une sauvegarde asynchrone en arrière-plan.
|
||||||
* Devrait être appelée lorsque l'application se met en pause ou est détruite,
|
* Les statistiques transitoires (partie en cours) ne sont pas sauvegardées.
|
||||||
* ou après une mise à jour significative des statistiques (ex: reset).
|
|
||||||
*/
|
*/
|
||||||
public void saveStats() {
|
public void saveStats() {
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
// Sauvegarde le high score (clé partagée)
|
|
||||||
editor.putInt(HIGH_SCORE_KEY, overallHighScore);
|
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_PLAYED, totalGamesPlayed);
|
||||||
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
|
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
|
||||||
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
|
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
|
||||||
@ -169,11 +164,9 @@ public class GameStats {
|
|||||||
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
|
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
|
||||||
editor.putInt(STATS_HIGHEST_TILE, highestTile);
|
editor.putInt(STATS_HIGHEST_TILE, highestTile);
|
||||||
editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached);
|
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_BEST_WINNING_TIME_MS, bestWinningTimeMs);
|
||||||
editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs);
|
editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs);
|
||||||
|
// Sauvegarde des stats Multi
|
||||||
// Sauvegarde les statistiques multijoueur (placeholders)
|
|
||||||
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
|
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
|
||||||
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
|
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
|
||||||
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
|
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
|
||||||
@ -181,29 +174,26 @@ public class GameStats {
|
|||||||
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
|
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
|
||||||
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
|
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
|
||||||
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
|
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
|
||||||
|
editor.apply(); // Sauvegarde asynchrone
|
||||||
// Applique les changements de manière asynchrone
|
|
||||||
editor.apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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").
|
* Enregistre le début d'une nouvelle partie solo.
|
||||||
* Incrémente le compteur de parties démarrées (`totalGamesStarted`) et réinitialise
|
* Incrémente le compteur de parties démarrées, réinitialise les compteurs de la partie actuelle
|
||||||
* les statistiques spécifiques à la partie en cours (`currentMoves`, `mergesThisGame`, `currentGameStartTimeMs`).
|
* (mouvements, fusions) et enregistre l'heure de début.
|
||||||
*/
|
*/
|
||||||
public void startGame() {
|
public void startGame() {
|
||||||
totalGamesStarted++;
|
totalGamesStarted++;
|
||||||
currentMoves = 0;
|
currentMoves = 0;
|
||||||
mergesThisGame = 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).
|
* Enregistre un mouvement effectué dans la partie solo actuelle.
|
||||||
* Incrémente le compteur de mouvements pour la partie en cours (`currentMoves`)
|
* Incrémente le compteur de mouvements pour la partie en cours et le compteur total de mouvements.
|
||||||
* et le compteur total de mouvements (`totalMoves`).
|
|
||||||
*/
|
*/
|
||||||
public void recordMove() {
|
public void recordMove() {
|
||||||
currentMoves++;
|
currentMoves++;
|
||||||
@ -211,11 +201,10 @@ public class GameStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
|
* Enregistre une ou plusieurs fusions effectuées lors d'un mouvement en solo.
|
||||||
* Incrémente le compteur de fusions pour la partie en cours (`mergesThisGame`)
|
* Incrémente le compteur de fusions pour la partie en cours et le compteur total de fusions.
|
||||||
* et le compteur total de fusions (`totalMerges`).
|
|
||||||
*
|
*
|
||||||
* @param numberOfMerges Le nombre de fusions distinctes réalisées lors du dernier mouvement (typiquement 1 ou plus).
|
* @param numberOfMerges Le nombre de fusions à ajouter (doit être > 0).
|
||||||
*/
|
*/
|
||||||
public void recordMerge(int numberOfMerges) {
|
public void recordMerge(int numberOfMerges) {
|
||||||
if (numberOfMerges > 0) {
|
if (numberOfMerges > 0) {
|
||||||
@ -225,86 +214,144 @@ public class GameStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour la statistique de la plus haute tuile atteinte globalement (`highestTile`),
|
* Met à jour la valeur de la plus haute tuile jamais atteinte si la nouvelle valeur est supérieure.
|
||||||
* si la valeur fournie est supérieure à la valeur actuelle enregistrée.
|
|
||||||
* Devrait être appelée après chaque mouvement qui pourrait créer une nouvelle tuile max.
|
|
||||||
*
|
*
|
||||||
* @param currentHighestTileValue La valeur de la tuile la plus haute sur le plateau à l'instant T.
|
* @param tileValue La valeur de la tuile potentiellement la plus haute.
|
||||||
*/
|
*/
|
||||||
public void updateHighestTile(int currentHighestTileValue) {
|
public void updateHighestTile(int tileValue) {
|
||||||
if (currentHighestTileValue > this.highestTile) {
|
if (tileValue > highestTile) {
|
||||||
this.highestTile = currentHighestTileValue;
|
highestTile = tileValue;
|
||||||
// La sauvegarde se fera via saveStats() globalement
|
saveStats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enregistre une victoire (atteinte de l'objectif >= 2048).
|
* Enregistre une victoire en mode solo (atteinte de l'objectif).
|
||||||
* Incrémente le compteur de victoires (`numberOfTimesObjectiveReached`) et met à jour
|
* Incrémente le compteur de victoires, met à jour les meilleurs/pires temps de victoire si applicable,
|
||||||
* les statistiques de meilleur/pire temps de victoire si nécessaire.
|
* et appelle {@link #endGame(long)} pour finaliser les statistiques de la partie.
|
||||||
* Appelle également {@link #endGame(long)} pour finaliser les statistiques générales de la partie.
|
|
||||||
*
|
*
|
||||||
* @param timeTakenMs Temps écoulé (en millisecondes) pour terminer cette partie gagnante.
|
* @param timeMs Le temps pris pour gagner cette partie (en millisecondes).
|
||||||
*/
|
*/
|
||||||
public void recordWin(long timeTakenMs) {
|
public void recordWin(long timeMs) {
|
||||||
numberOfTimesObjectiveReached++;
|
numberOfTimesObjectiveReached++;
|
||||||
if (timeTakenMs < bestWinningTimeMs) {
|
if (timeMs < bestWinningTimeMs) {
|
||||||
bestWinningTimeMs = timeTakenMs;
|
bestWinningTimeMs = timeMs;
|
||||||
}
|
}
|
||||||
// Utiliser >= pour le pire temps permet de l'enregistrer même si c'est le premier
|
// Utilise >= pour worstWinningTimeMs pour s'assurer qu'il est mis à jour même si le premier temps est 0
|
||||||
if (timeTakenMs >= worstWinningTimeMs) {
|
if (timeMs >= worstWinningTimeMs) {
|
||||||
worstWinningTimeMs = timeTakenMs;
|
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).
|
* Enregistre une défaite en mode solo.
|
||||||
* Calcule le temps écoulé pour la partie et appelle {@link #endGame(long)}
|
* Calcule le temps de jeu pour cette partie et appelle {@link #endGame(long)}.
|
||||||
* pour finaliser les statistiques générales.
|
|
||||||
*/
|
*/
|
||||||
public void recordLoss() {
|
public void recordLoss() {
|
||||||
// Calcule le temps écoulé pour cette partie avant de la finaliser
|
long timeMs = (currentGameStartTimeMs > 0) ? System.currentTimeMillis() - currentGameStartTimeMs : 0;
|
||||||
long timeTakenMs = System.currentTimeMillis() - currentGameStartTimeMs;
|
endGame(timeMs); // Finalise la partie
|
||||||
endGame(timeTakenMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
|
* Méthode privée pour finaliser les statistiques communes à la fin d'une partie solo (victoire ou défaite).
|
||||||
* Incrémente le compteur de parties jouées (`totalGamesPlayed`) et ajoute le temps
|
* Incrémente le compteur total de parties jouées et ajoute le temps de jeu de cette partie.
|
||||||
* de la partie terminée au temps de jeu total (`totalPlayTimeMs`).
|
|
||||||
*
|
*
|
||||||
* @param timeTakenMs Temps total (en millisecondes) de la partie qui vient de se terminer.
|
* @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++;
|
totalGamesPlayed++;
|
||||||
addPlayTime(timeTakenMs); // Ajoute la durée de la partie terminée au total
|
addPlayTime(timeMs);
|
||||||
// Ne pas réinitialiser currentGameStartTimeMs ici, car une nouvelle partie
|
currentGameStartTimeMs = 0;
|
||||||
// pourrait ne pas commencer immédiatement (ex: affichage dialogue Game Over).
|
saveStats();
|
||||||
// startGame() s'en chargera.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ajoute une durée (en millisecondes) au temps de jeu total enregistré (`totalPlayTimeMs`).
|
* Ajoute une durée au temps de jeu total cumulé en mode solo.
|
||||||
* Typiquement appelé dans `onPause` de l'activité pour enregistrer le temps écoulé
|
|
||||||
* depuis `onResume` ou `startGame`.
|
|
||||||
*
|
*
|
||||||
* @param durationMs Durée positive (en millisecondes) à ajouter au temps total.
|
* @param durationMs La durée à ajouter (en millisecondes, doit être > 0).
|
||||||
*/
|
*/
|
||||||
public void addPlayTime(long durationMs) {
|
public void addPlayTime(long durationMs) {
|
||||||
if (durationMs > 0) {
|
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)
|
* Méthode privée pour mettre à jour les statistiques communes à la fin d'une partie multijoueur.
|
||||||
* à leurs valeurs par défaut (0 ou équivalent).
|
* Met à jour le nombre de parties jouées, le score total, le temps total et le meilleur score multi.
|
||||||
* Après la réinitialisation, les nouvelles valeurs sont immédiatement sauvegardées
|
*
|
||||||
* dans les SharedPreferences via {@link #saveStats()}.
|
* @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() {
|
public void resetStats() {
|
||||||
// Réinitialise toutes les variables membres à 0 ou valeur initiale
|
// Réinitialisation Solo & Général
|
||||||
overallHighScore = 0;
|
overallHighScore = 0;
|
||||||
totalGamesPlayed = 0;
|
totalGamesPlayed = 0;
|
||||||
totalGamesStarted = 0;
|
totalGamesStarted = 0;
|
||||||
@ -313,11 +360,9 @@ public class GameStats {
|
|||||||
totalMerges = 0;
|
totalMerges = 0;
|
||||||
highestTile = 0;
|
highestTile = 0;
|
||||||
numberOfTimesObjectiveReached = 0;
|
numberOfTimesObjectiveReached = 0;
|
||||||
// perfectGames = 0; // Supprimé
|
|
||||||
bestWinningTimeMs = Long.MAX_VALUE;
|
bestWinningTimeMs = Long.MAX_VALUE;
|
||||||
worstWinningTimeMs = 0L;
|
worstWinningTimeMs = 0L;
|
||||||
|
// Réinitialisation Multi
|
||||||
// Réinitialise les stats multijoueur (placeholders)
|
|
||||||
multiplayerGamesWon = 0;
|
multiplayerGamesWon = 0;
|
||||||
multiplayerGamesPlayed = 0;
|
multiplayerGamesPlayed = 0;
|
||||||
multiplayerBestWinningStreak = 0;
|
multiplayerBestWinningStreak = 0;
|
||||||
@ -325,96 +370,94 @@ public class GameStats {
|
|||||||
multiplayerTotalTimeMs = 0L;
|
multiplayerTotalTimeMs = 0L;
|
||||||
totalMultiplayerLosses = 0;
|
totalMultiplayerLosses = 0;
|
||||||
multiplayerHighestScore = 0;
|
multiplayerHighestScore = 0;
|
||||||
|
// Réinitialisation Transitoire
|
||||||
// Réinitialise les stats de la partie en cours (au cas où)
|
|
||||||
currentMoves = 0;
|
currentMoves = 0;
|
||||||
mergesThisGame = 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(); // Sauvegarde l'état réinitialisé
|
||||||
saveStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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; }
|
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; }
|
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; }
|
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; }
|
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; }
|
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; }
|
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; }
|
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; }
|
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; }
|
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 getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; }
|
||||||
// public int getPerfectGames() { return perfectGames; } // Supprimé
|
/** @return Le meilleur temps pour atteindre l'objectif en solo (millisecondes), ou Long.MAX_VALUE si jamais atteint. */
|
||||||
/** @return Le meilleur temps (le plus court, en ms) pour gagner une partie, ou Long.MAX_VALUE si aucune victoire. */
|
|
||||||
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
|
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; }
|
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
|
||||||
/** @return Le meilleur score global enregistré. */
|
/** @return Le meilleur score global atteint (principalement solo). */
|
||||||
public int getOverallHighScore() { return overallHighScore; }
|
public int getOverallHighScore() { return overallHighScore; }
|
||||||
|
|
||||||
// Getters Multiplayer (fonctionnalité future)
|
// --- Getters Multijoueur ---
|
||||||
/** @return Nombre de parties multijoueur gagnées. */
|
/** @return Le nombre total de parties multijoueur gagnées. */
|
||||||
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
|
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; }
|
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; }
|
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
|
||||||
/** @return Score total accumulé en multijoueur. */
|
/** @return Le score total cumulé en multijoueur. */
|
||||||
public long getMultiplayerTotalScore() { return multiplayerTotalScore; }
|
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; }
|
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; }
|
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; }
|
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`).
|
* Met à jour le meilleur score global si le score fourni est supérieur.
|
||||||
* La mise à jour n'a lieu que si le score fourni est strictement supérieur
|
* Utile pour enregistrer un nouveau record après une partie solo.
|
||||||
* au meilleur score actuel. La sauvegarde effective dans SharedPreferences
|
* Ne sauvegarde pas automatiquement, appeler {@link #saveStats()} si nécessaire.
|
||||||
* se fait via un appel ultérieur à {@link #saveStats()}.
|
|
||||||
*
|
*
|
||||||
* @param highScore Le nouveau score à potentiellement définir comme meilleur score.
|
* @param score Le nouveau score potentiellement plus élevé.
|
||||||
*/
|
*/
|
||||||
public void setHighestScore(int highScore) {
|
public void setHighestScore(int score) {
|
||||||
// Met à jour seulement si la nouvelle valeur est meilleure
|
if (score > overallHighScore) {
|
||||||
if (highScore > this.overallHighScore) {
|
overallHighScore = score;
|
||||||
this.overallHighScore = highScore;
|
|
||||||
// La sauvegarde se fait via saveStats() globalement, pas ici directement.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Définit le timestamp (en ms) du début de la partie en cours.
|
* Définit manuellement l'heure de début de la partie en cours.
|
||||||
* Typiquement utilisé par MainActivity dans `onResume` pour reprendre le chronomètre.
|
* 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) {
|
public void setCurrentGameStartTimeMs(long timeMs) {
|
||||||
this.currentGameStartTimeMs = timeMs;
|
currentGameStartTimeMs = timeMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Méthodes Calculées ---
|
// --- 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() {
|
public long getAverageGameTimeMs() {
|
||||||
return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0L;
|
return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0L;
|
||||||
@ -422,44 +465,44 @@ public class GameStats {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule le score moyen par partie multijoueur terminée.
|
* 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() {
|
public int getMultiplayerAverageScore() {
|
||||||
return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0;
|
return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule le temps moyen passé par partie multijoueur terminée.
|
* Calcule le temps de jeu moyen par partie multijoueur terminée.
|
||||||
* (Basé sur les statistiques multijoueur placeholder).
|
|
||||||
*
|
*
|
||||||
* @return Le temps moyen par partie multijoueur en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée.
|
* @return Le temps moyen en millisecondes, ou 0 si aucune partie multijoueur n'a été jouée.
|
||||||
*/
|
*/
|
||||||
public long getMultiplayerAverageTimeMs() {
|
public long getMultiplayerAverageTimeMs() {
|
||||||
return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0L;
|
return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formate une durée donnée en millisecondes en une chaîne de caractères lisible.
|
* Formate une durée en millisecondes en une chaîne lisible "HH:MM:SS" ou "MM:SS".
|
||||||
* Le format est "hh:mm:ss" si la durée est d'une heure ou plus, sinon "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.
|
* @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
|
@SuppressLint("DefaultLocale") // Le format %02d est indépendant de la locale
|
||||||
public static String formatTime(long milliseconds) {
|
public static String formatTime(long ms) {
|
||||||
if (milliseconds < 0) { milliseconds = 0; } // Gère les cas négatifs
|
if (ms < 0) {
|
||||||
|
ms = 0; // Traite les durées négatives comme 0
|
||||||
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
|
}
|
||||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; // Minutes restantes après les heures
|
long hours = TimeUnit.MILLISECONDS.toHours(ms);
|
||||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60; // Secondes restantes après les minutes
|
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) {
|
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);
|
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
|
||||||
} else {
|
} else {
|
||||||
// Format minutes:secondes sinon
|
// Format sans heures si la durée est < 1 heure
|
||||||
return String.format("%02d:%02d", minutes, seconds);
|
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;
|
package legion.muyue.best2048;
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
@ -11,143 +5,128 @@ import android.app.NotificationManager;
|
|||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
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.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.annotation.NonNull;
|
||||||
import androidx.core.app.ActivityCompat; // Pour checkSelfPermission (remplace ContextCompat ici)
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
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 {
|
public class NotificationHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifiant unique et constant pour le canal de notification de cette application.
|
* Identifiant unique et constant pour le canal de notification de l'application.
|
||||||
* Ce même ID doit être utilisé lors de la création de la notification via {@link NotificationCompat.Builder}.
|
* Requis pour Android 8.0 (API niveau 26) et supérieur.
|
||||||
* Il est visible par l'utilisateur dans les paramètres de notification de l'application sur Android 8.0+.
|
|
||||||
*/
|
*/
|
||||||
public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé dans MainActivity/Service
|
public static final String CHANNEL_ID = "BEST_2048_CHANNEL";
|
||||||
|
|
||||||
/** Tag pour les logs spécifiques à ce helper. */
|
|
||||||
private static final String TAG = "NotificationHelper";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée le canal de notification requis pour afficher des notifications sur Android 8.0 (API 26) et supérieur.
|
* Constructeur privé pour empêcher l'instanciation de cette classe utilitaire.
|
||||||
* 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`
|
private NotificationHelper() {
|
||||||
* ou dans `MainActivity.onCreate`), avant d'afficher la toute première notification sur un appareil API 26+.
|
// 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) {
|
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) {
|
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);
|
CharSequence name = context.getString(R.string.notification_channel_name);
|
||||||
String description = context.getString(R.string.notification_channel_description);
|
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)
|
// Définit l'importance du canal (affecte comment la notification est présentée)
|
||||||
// IMPORTANCE_DEFAULT est un bon point de départ pour la plupart des notifications.
|
int importance = NotificationManager.IMPORTANCE_DEFAULT; // Ou IMPORTANCE_HIGH, etc.
|
||||||
int importance = NotificationManager.IMPORTANCE_DEFAULT;
|
|
||||||
|
|
||||||
// Crée l'objet NotificationChannel
|
// Crée l'objet NotificationChannel
|
||||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
||||||
channel.setDescription(description);
|
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
|
// Récupère le NotificationManager système
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
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.
|
// Enregistre le canal auprès du système. Si le canal existe déjà, aucune opération n'est effectuée.
|
||||||
if (notificationManager != null) {
|
if (notificationManager != null) {
|
||||||
Log.d(TAG, "Création ou mise à jour du canal de notification: " + CHANNEL_ID);
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
} else {
|
} 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.
|
* Construit et affiche une notification standard pour l'application.
|
||||||
* Vérifie la permission {@code POST_NOTIFICATIONS} sur Android 13 (API 33) et supérieur
|
* Gère la vérification de la permission {@code POST_NOTIFICATIONS} pour Android 13+ (Tiramisu).
|
||||||
* avant de tenter d'afficher la notification. Si la permission est manquante,
|
* Configure un {@link PendingIntent} pour ouvrir {@link MainActivity} lorsque l'utilisateur
|
||||||
* un message d'erreur est logué et la méthode se termine sans afficher de notification.
|
* 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.)
|
* @param context Le contexte de l'application. Ne doit pas être null.
|
||||||
* utilisé pour construire la notification et accéder aux ressources.
|
* @param title Le titre de la notification. Ne doit pas être null.
|
||||||
* @param title Le titre à afficher dans la notification.
|
* @param message Le message principal (corps) de la notification. Ne doit pas être null.
|
||||||
* @param message Le contenu textuel principal de la notification.
|
* @param notificationId Un identifiant entier unique pour cette notification. Si une notification
|
||||||
* @param notificationId Un identifiant entier unique pour cette notification. Permet de mettre à jour
|
* avec le même ID existe déjà, elle sera mise à jour.
|
||||||
* ou d'annuler cette notification spécifique plus tard en utilisant le même ID.
|
|
||||||
*/
|
*/
|
||||||
public static void showNotification(@NonNull Context context, @NonNull String title, @NonNull String message, int notificationId) {
|
public static void showNotification(@NonNull Context context, @NonNull String title, @NonNull String message, int notificationId) {
|
||||||
Log.d(TAG, "Tentative d'affichage de la notification ID: " + notificationId + " Titre: " + title);
|
|
||||||
|
|
||||||
// --- Vérification de la permission pour Android 13 (API 33) et supérieur ---
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (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) {
|
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.
|
// Si la permission n'est pas accordée, on ne peut pas afficher la notification.
|
||||||
// L'application appelante (ex: MainActivity, Service) est responsable de demander la permission
|
// L'application devrait demander la permission à l'utilisateur à un moment approprié.
|
||||||
// si l'utilisateur a indiqué vouloir recevoir des notifications.
|
return; // Arrête l'exécution de la méthode
|
||||||
Log.w(TAG, "Permission POST_NOTIFICATIONS manquante pour API 33+. Notification ID " + notificationId + " non affichée.");
|
|
||||||
// Optionnel : Envoyer un broadcast ou utiliser une autre méthode pour signaler l'échec ? Non, simple log suffit ici.
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Permission POST_NOTIFICATIONS accordée sur API 33+.");
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(TAG, "API < 33, permission POST_NOTIFICATIONS non requise pour afficher.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Création de l'Intent pour le clic sur la notification ---
|
// Crée un Intent pour ouvrir MainActivity lorsque la notification est cliquée
|
||||||
// Ouvre MainActivity lorsque l'utilisateur clique sur la notification.
|
|
||||||
Intent intent = new Intent(context, MainActivity.class);
|
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);
|
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;
|
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
|
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
|
||||||
}
|
}
|
||||||
// Crée le PendingIntent qui enveloppe l'Intent
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, pendingIntentFlags);
|
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)
|
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)
|
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Icône obligatoire
|
||||||
// .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher)) // Icône optionnelle plus grande
|
|
||||||
.setContentTitle(title) // Titre de la notification
|
.setContentTitle(title) // Titre de la notification
|
||||||
.setContentText(message) // Texte principal
|
.setContentText(message) // Corps du message
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité (affecte l'affichage head-up, etc.)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité (affecte l'affichage)
|
||||||
.setContentIntent(pendingIntent) // Action à exécuter au clic
|
.setContentIntent(pendingIntent) // Action lorsque l'utilisateur clique sur la notification
|
||||||
.setAutoCancel(true) // Ferme automatiquement la notification après le clic
|
.setAutoCancel(true) // Ferme la notification après le clic
|
||||||
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) // Utilise son/vibration par défaut du système (si autorisé)
|
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) // Son et vibration par défaut
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); // Visible sur l'écran de verrouillage
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); // Visible sur l'écran de verrouillage
|
||||||
|
|
||||||
// --- Affichage de la notification ---
|
// Récupère NotificationManagerCompat pour envoyer la notification
|
||||||
// Utilise NotificationManagerCompat pour la compatibilité
|
|
||||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Affiche la notification. Si une notification avec le même ID existe déjà, elle est mise à jour.
|
|
||||||
notificationManager.notify(notificationId, builder.build());
|
notificationManager.notify(notificationId, builder.build());
|
||||||
Log.i(TAG, "Notification ID " + notificationId + " affichée avec succès.");
|
|
||||||
} catch (SecurityException e){
|
} 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) {
|
} 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;
|
package legion.muyue.best2048;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -12,66 +6,85 @@ import android.view.GestureDetector;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import androidx.annotation.NonNull;
|
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 {
|
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;
|
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;
|
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 {
|
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();
|
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();
|
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();
|
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();
|
void onSwipeRight();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructeur.
|
* Construit une nouvelle instance de l'écouteur de balayage.
|
||||||
* @param context Contexte applicatif, nécessaire pour `GestureDetector`.
|
*
|
||||||
* @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null.
|
* @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) {
|
public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) {
|
||||||
this.gestureDetector = new GestureDetector(context, new GestureListener());
|
this.gestureDetector = new GestureDetector(context, new GestureListener());
|
||||||
|
// Stocke la référence vers l'écouteur fourni
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercepte les événements tactiles sur la vue associée et les délègue
|
* Méthode appelée lorsque la {@link View} associée reçoit un événement tactile.
|
||||||
* au {@link GestureDetector} pour analyse.
|
* Délègue le traitement de l'événement au {@link GestureDetector}.
|
||||||
* @param v La vue touchée.
|
*
|
||||||
* @param event L'événement tactile.
|
* @param v La {@link View} qui a reçu l'événement tactile.
|
||||||
* @return true si le geste a été consommé par le détecteur, false sinon.
|
* @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")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
// Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling),
|
// Passe l'événement tactile au GestureDetector pour analyse
|
||||||
// il retournera true, et l'événement ne sera pas propagé davantage.
|
|
||||||
return gestureDetector.onTouchEvent(event);
|
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 {
|
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;
|
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;
|
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toujours retourner true pour onDown garantit que les événements suivants
|
* Appelée lorsque l'événement {@link MotionEvent#ACTION_DOWN} se produit.
|
||||||
* (comme onFling) seront bien reçus par ce listener.
|
* 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
|
@Override
|
||||||
public boolean onDown(@NonNull MotionEvent e) {
|
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é.
|
* Appelée lorsque le {@link GestureDetector} détecte un mouvement de "fling"
|
||||||
* Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide
|
* (un glissement rapide suivi d'un relâchement). C'est ici que la logique
|
||||||
* et notifie le {@link SwipeListener} externe.
|
* 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
|
@Override
|
||||||
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
|
||||||
if (e1 == null) return false; // Point de départ est nécessaire
|
// 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 {
|
try {
|
||||||
|
// Calcule la différence de position entre le début et la fin du mouvement
|
||||||
float diffY = e2.getY() - e1.getY();
|
float diffY = e2.getY() - e1.getY();
|
||||||
float diffX = e2.getX() - e1.getX();
|
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)) {
|
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||||
// Mouvement principalement horizontal
|
// 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 (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
if (diffX > 0) {
|
if (diffX > 0) {
|
||||||
|
// Balayage vers la droite
|
||||||
listener.onSwipeRight();
|
listener.onSwipeRight();
|
||||||
} else {
|
} else {
|
||||||
|
// Balayage vers la gauche
|
||||||
listener.onSwipeLeft();
|
listener.onSwipeLeft();
|
||||||
}
|
}
|
||||||
result = true; // Geste horizontal traité
|
result = true; // Un balayage horizontal a été traité
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mouvement principalement vertical
|
// 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 (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
if (diffY > 0) {
|
if (diffY > 0) {
|
||||||
|
// Balayage vers le bas
|
||||||
listener.onSwipeBottom();
|
listener.onSwipeBottom();
|
||||||
} else {
|
} else {
|
||||||
|
// Balayage vers le haut
|
||||||
listener.onSwipeTop();
|
listener.onSwipeTop();
|
||||||
}
|
}
|
||||||
result = true; // Geste vertical traité
|
result = true; // Un balayage vertical a été traité
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} 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;
|
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;
|
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 {
|
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;
|
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;
|
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")
|
@SerializedName("player1Id")
|
||||||
private String 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; }
|
* L'identifiant du deuxième joueur.
|
||||||
public String getStatus() { return status; }
|
* Peut être null si le joueur n'a pas encore rejoint ou s'il s'agit d'une partie solo.
|
||||||
public String getPlayer1Id() { return player1Id; }
|
* Utilisé par Gson pour la sérialisation avec le nom "player2Id".
|
||||||
public String getPlayer2Id() { return 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;
|
package legion.muyue.best2048.data;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
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 {
|
public class GameStateResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L'identifiant unique de la partie.
|
||||||
|
* Utilisé par Gson pour la sérialisation avec le nom "gameId".
|
||||||
|
*/
|
||||||
@SerializedName("gameId")
|
@SerializedName("gameId")
|
||||||
private String 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")
|
@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")
|
@SerializedName("player1Score")
|
||||||
private int player1Score;
|
private int player1Score;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le score actuel du joueur 2.
|
||||||
|
* Utilisé par Gson pour la sérialisation avec le nom "player2Score".
|
||||||
|
*/
|
||||||
@SerializedName("player2Score")
|
@SerializedName("player2Score")
|
||||||
private int 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;
|
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")
|
@SerializedName("isGameOver")
|
||||||
private boolean 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;
|
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")
|
@SerializedName("status")
|
||||||
private String 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;
|
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;
|
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;
|
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; }
|
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; }
|
public int[][] getBoard() { return board; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le score du joueur 1.
|
||||||
|
*
|
||||||
|
* @return Le score du joueur 1.
|
||||||
|
*/
|
||||||
public int getPlayer1Score() { return player1Score; }
|
public int getPlayer1Score() { return player1Score; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le score du joueur 2.
|
||||||
|
*
|
||||||
|
* @return Le score du joueur 2.
|
||||||
|
*/
|
||||||
public int getPlayer2Score() { return player2Score; }
|
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; }
|
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; }
|
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; }
|
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; }
|
public String getStatus() { return status; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'identifiant du joueur 1.
|
||||||
|
*
|
||||||
|
* @return L'identifiant du joueur 1.
|
||||||
|
*/
|
||||||
public String getPlayer1Id() { return player1Id; }
|
public String getPlayer1Id() { return player1Id; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'identifiant du joueur 2.
|
||||||
|
*
|
||||||
|
* @return L'identifiant du joueur 2.
|
||||||
|
*/
|
||||||
public String getPlayer2Id() { return player2Id; }
|
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; }
|
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) {
|
public int getOpponentScore(String myActualPlayerId) {
|
||||||
if (myActualPlayerId == null) return 0;
|
if (myActualPlayerId == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (myActualPlayerId.equals(player1Id)) {
|
if (myActualPlayerId.equals(player1Id)) {
|
||||||
// Si je suis P1, le score de l'adversaire est P2
|
|
||||||
return player2Score;
|
return player2Score;
|
||||||
} else if (myActualPlayerId.equals(player2Id)) {
|
} else if (myActualPlayerId.equals(player2Id)) {
|
||||||
// Si je suis P2, le score de l'adversaire est P1
|
|
||||||
return player1Score;
|
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) {
|
public int getMyScore(String myActualPlayerId) {
|
||||||
if (myActualPlayerId == null) return 0;
|
if (myActualPlayerId == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (myActualPlayerId.equals(player1Id)) {
|
if (myActualPlayerId.equals(player1Id)) {
|
||||||
return player1Score;
|
return player1Score;
|
||||||
} else if (myActualPlayerId.equals(player2Id)) {
|
} else if (myActualPlayerId.equals(player2Id)) {
|
||||||
return player2Score;
|
return player2Score;
|
||||||
}
|
}
|
||||||
return 0; // Mon ID ne correspond à aucun joueur ?
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,39 @@
|
|||||||
package legion.muyue.best2048.data;
|
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 {
|
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) {
|
public MoveRequest(String direction, String playerId) {
|
||||||
this.direction = direction;
|
this.direction = direction;
|
||||||
this.playerId = playerId;
|
this.playerId = playerId;
|
||||||
}
|
}
|
||||||
// Pas besoin de getters si seulement utilisé pour l'envoi avec Gson
|
|
||||||
}
|
}
|
@ -1,10 +1,27 @@
|
|||||||
package legion.muyue.best2048.data;
|
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 {
|
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;
|
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) {
|
public PlayerIdRequest(String playerId) {
|
||||||
this.playerId = 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.Retrofit;
|
||||||
import retrofit2.converter.gson.GsonConverterFactory;
|
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 {
|
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/";
|
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;
|
private static Retrofit retrofit = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée et retourne une instance singleton de Retrofit configurée.
|
* Constructeur privé pour empêcher l'instanciation directe de cette classe utilitaire.
|
||||||
* Inclut un intercepteur pour logger les requêtes/réponses HTTP (utile pour le debug).
|
*/
|
||||||
|
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() {
|
public static Retrofit getClient() {
|
||||||
if (retrofit == null) {
|
if (retrofit == null) {
|
||||||
// Intercepteur pour voir les logs HTTP dans Logcat (Niveau BODY pour tout voir)
|
|
||||||
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||||
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||||
|
|
||||||
// Client OkHttp avec l'intercepteur
|
// Construit le client OkHttp en ajoutant l'intercepteur
|
||||||
OkHttpClient client = new OkHttpClient.Builder()
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
.addInterceptor(logging)
|
.addInterceptor(logging)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Construction de l'instance Retrofit
|
// Construit l'instance Retrofit
|
||||||
retrofit = new Retrofit.Builder()
|
retrofit = new Retrofit.Builder()
|
||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
.client(client) // Utilise le client OkHttp configuré
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create()) // Utilise Gson pour parser le JSON
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
return retrofit;
|
return retrofit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fournit une instance de l'interface ApiService.
|
* Crée et retourne une instance de l'interface {@link ApiService}.
|
||||||
* @return Instance de 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() {
|
public static ApiService getApiService() {
|
||||||
return getClient().create(ApiService.class);
|
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.GameInfo;
|
||||||
import legion.muyue.best2048.data.GameStateResponse;
|
import legion.muyue.best2048.data.GameStateResponse;
|
||||||
import legion.muyue.best2048.data.MoveRequest;
|
import legion.muyue.best2048.data.MoveRequest;
|
||||||
|
import legion.muyue.best2048.data.PlayerIdRequest;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
import retrofit2.http.Path;
|
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 {
|
public interface ApiService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée une nouvelle partie ou rejoint une partie en attente (matchmaking simple).
|
* Crée une nouvelle partie ou rejoint une partie existante en attente pour le joueur spécifié.
|
||||||
* TODO: Définir les paramètres nécessaires (ex: ID du joueur).
|
* Effectue une requête POST vers {@code /api/games}.
|
||||||
* @return Informations sur la partie créée/rejointe.
|
* 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);
|
Call<GameInfo> createOrJoinGame(@Body PlayerIdRequest playerIdRequest);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère l'état actuel complet d'une partie spécifique.
|
* Récupère l'état actuel complet d'une partie spécifique.
|
||||||
* @param gameId L'identifiant unique de la partie.
|
* Effectue une requête GET vers {@code /api/games/{gameId}}.
|
||||||
* @return L'état actuel du jeu.
|
*
|
||||||
|
* @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}")
|
@GET("games/{gameId}")
|
||||||
Call<GameStateResponse> getGameState(@Path("gameId") String gameId);
|
Call<GameStateResponse> getGameState(@Path("gameId") String gameId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soumet le mouvement d'un joueur pour une partie spécifique.
|
* Soumet un mouvement effectué par un joueur dans une partie spécifique.
|
||||||
* Le serveur validera si c'est bien le tour de ce joueur.
|
* Effectue une requête POST vers {@code /api/games/{gameId}/moves}.
|
||||||
* @param gameId L'identifiant unique de la partie.
|
* Le corps de la requête contient les détails du mouvement (direction et identifiant du joueur).
|
||||||
* @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).
|
* @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")
|
@POST("games/{gameId}/moves")
|
||||||
Call<GameStateResponse> makeMove(@Path("gameId") String gameId, @Body MoveRequest moveRequest);
|
Call<GameStateResponse> makeMove(@Path("gameId") String gameId, @Body MoveRequest moveRequest);
|
||||||
|
@ -42,53 +42,16 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/general_section" />
|
android:text="@string/general_section" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="@style/SectionContainer"
|
style="@style/SectionContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
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
|
<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" />
|
||||||
android:id="@+id/high_score_stats_label"
|
<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" />
|
||||||
style="@style/StatLabel"
|
<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" />
|
||||||
android:layout_width="match_parent"
|
<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" />
|
||||||
android:layout_height="wrap_content"
|
<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" />
|
||||||
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>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -96,32 +59,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/current_game_section" />
|
android:text="@string/current_game_section" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="@style/SectionContainer"
|
style="@style/SectionContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
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
|
<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" />
|
||||||
android:id="@+id/current_moves_label"
|
<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" />
|
||||||
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>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -129,60 +73,16 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/single_player_section" />
|
android:text="@string/single_player_section" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="@style/SectionContainer"
|
style="@style/SectionContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
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
|
<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" />
|
||||||
android:id="@+id/average_game_time_label"
|
<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" />
|
||||||
style="@style/StatLabel"
|
<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" />
|
||||||
android:layout_width="match_parent"
|
<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" />
|
||||||
android:layout_height="wrap_content"
|
<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" />
|
||||||
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" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -190,67 +90,23 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/multiplayer_section" />
|
android:text="@string/multiplayer_section" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="@style/SectionContainer"
|
style="@style/SectionContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
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
|
<TextView
|
||||||
android:id="@+id/multiplayer_games_won_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/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"
|
|
||||||
style="@style/StatLabel"
|
style="@style/StatLabel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/average_time_per_game_label" />
|
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
|
<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" />
|
||||||
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>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Best 2048</string>
|
<string name="app_name">Best 2048</string>
|
||||||
<string name="name_2048">2048</string>
|
<string name="name_2048">2048</string>
|
||||||
<string name="score">Score :</string>
|
<string name="score">Score:</string>
|
||||||
<string name="score_placeholder">Score :\n%d</string>
|
<string name="score_placeholder">Score:\n%d</string>
|
||||||
<string name="high_score_placeholder">High Score :\n%d</string>
|
<string name="high_score_placeholder">High Score:\n%d</string>
|
||||||
<string name="high_score">High Score :</string>
|
<string name="high_score">High Score:</string>
|
||||||
<string name="restart">Restart</string>
|
<string name="restart">Restart</string>
|
||||||
<string name="stats">Stats</string>
|
<string name="stats">Stats</string>
|
||||||
<string name="menu">Menu</string>
|
<string name="menu">Menu</string>
|
||||||
<string name="multiplayer">Multiplayer</string>
|
<string name="multiplayer">Multiplayer</string>
|
||||||
<string name="restart_confirm_title">Restart ?</string>
|
<string name="restart_confirm_title">Restart?</string>
|
||||||
<string name="restart_confirm_message">Are you sure you want to restart the game ?</string>
|
<string name="restart_confirm_message">Are you sure you want to restart the game?</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="confirm">Confirm</string>
|
<string name="confirm">Confirm</string>
|
||||||
<string name="high_score_stats">High Score: %d</string>
|
<string name="high_score_stats">High Score: %d</string>
|
||||||
@ -27,15 +28,15 @@
|
|||||||
<string name="worst_winning_time">Worst Winning Time: %s</string>
|
<string name="worst_winning_time">Worst Winning Time: %s</string>
|
||||||
<string name="total_merges">Total Merges: %d</string>
|
<string name="total_merges">Total Merges: %d</string>
|
||||||
<string name="highest_tile">Highest Tile: %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="number_of_time_objective_reached">Times Objective Reached: %d</string>
|
||||||
<string name="perfect_games">Perfect game : %d</string>
|
<string name="perfect_games">Perfect Games: %d</string>
|
||||||
<string name="multiplayer_games_won">Multiplayer game won : %d</string>
|
<string name="multiplayer_games_won">Multiplayer Games Won: %d</string>
|
||||||
<string name="multiplayer_games_played">Multiplayer game played : %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_win_rate">Multiplayer Win Rate: %s</string>
|
||||||
<string name="multiplayer_best_winning_streak">Best winning streak : %d</string>
|
<string name="multiplayer_best_winning_streak">Best Winning Streak: %d</string>
|
||||||
<string name="multiplayer_average_score">Multiplayer Average Score: %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="average_time_per_game_label">Average Time Per Game: %s</string>
|
||||||
<string name="total_multiplayer_losses">Total Multiplayer losses: %d</string>
|
<string name="total_multiplayer_losses">Total Multiplayer Losses: %d</string>
|
||||||
<string name="multiplayer_high_score">Multiplayer High Score: %d</string>
|
<string name="multiplayer_high_score">Multiplayer High Score: %d</string>
|
||||||
<string name="stats_button_label">Stats</string>
|
<string name="stats_button_label">Stats</string>
|
||||||
<string name="stats_title">Statistics</string>
|
<string name="stats_title">Statistics</string>
|
||||||
@ -44,13 +45,13 @@
|
|||||||
<string name="single_player_section">Single Player</string>
|
<string name="single_player_section">Single Player</string>
|
||||||
<string name="multiplayer_section">Multiplayer</string>
|
<string name="multiplayer_section">Multiplayer</string>
|
||||||
<string name="back_button_label">Back</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="you_won_message">Congratulations, you\'ve reached 2048!</string>
|
||||||
<string name="keep_playing">Continue</string>
|
<string name="keep_playing">Keep Playing</string>
|
||||||
<string name="new_game">New Part</string>
|
<string name="new_game">New Game</string>
|
||||||
<string name="game_over_title">Game over!</string>
|
<string name="game_over_title">Game Over!</string>
|
||||||
<string name="game_over_message">No move possible.\nFinal score: %d</string>
|
<string name="game_over_message">No more moves possible.\nFinal Score: %d</string>
|
||||||
<string name="quit">To leave</string>
|
<string name="quit">Quit</string>
|
||||||
<string name="menu_title">Main Menu</string>
|
<string name="menu_title">Main Menu</string>
|
||||||
<string name="menu_option_how_to_play">How to Play</string>
|
<string name="menu_option_how_to_play">How to Play</string>
|
||||||
<string name="menu_option_settings">Settings</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="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_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_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="ok">OK</string>
|
||||||
<string name="settings_title">Settings</string>
|
<string name="settings_title">Settings</string>
|
||||||
<string name="settings_sound_label">Sound</string>
|
<string name="settings_sound_label">Sound</string>
|
||||||
<string name="settings_notifications_label">Notifications</string>
|
<string name="settings_notifications_label">Notifications</string>
|
||||||
<string name="settings_permissions_button">Manage Permissions</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_reset_stats_button">Reset Statistics</string>
|
||||||
<string name="settings_quit_app_button">Quit Application</string>
|
<string name="settings_quit_app_button">Quit Application</string>
|
||||||
<string name="settings_close_button">Close</string>
|
<string name="settings_close_button">Close</string>
|
||||||
<string name="reset_stats_confirm_title">Reset Stats?</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="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_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="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>
|
<string name="stats_reset_confirmation">Statistics reset.</string>
|
||||||
@ -94,29 +96,30 @@
|
|||||||
<string name="sound_enabled">Sound effects enabled.</string>
|
<string name="sound_enabled">Sound effects enabled.</string>
|
||||||
<string name="sound_disabled">Sound effects disabled.</string>
|
<string name="sound_disabled">Sound effects disabled.</string>
|
||||||
|
|
||||||
<string name="multiplayer_status_searching">Recherche d\'une partie…</string>
|
<string name="multiplayer_status_searching">Searching for a game…</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_found">Game found! ID: %s…</string>
|
||||||
<string name="multiplayer_status_waiting_state">Connecté. En attente de l\'état du jeu…</string>
|
<string name="multiplayer_status_connecting">Connecting to server…</string>
|
||||||
<string name="multiplayer_status_waiting_opponent">En attente du coup adverse…</string>
|
<string name="multiplayer_status_waiting_state">Connected. Waiting for game state…</string>
|
||||||
<string name="multiplayer_status_sending_move">Envoi du mouvement…</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_yours">Your Turn</string>
|
||||||
<string name="multiplayer_turn_opponent">Tour Adversaire</string>
|
<string name="multiplayer_turn_opponent">Opponent\'s Turn</string>
|
||||||
|
|
||||||
<string name="multiplayer_my_score">Moi :\n%d</string>
|
<string name="multiplayer_my_score">You:\n%d</string>
|
||||||
<string name="multiplayer_opponent_score">Autre :\n%d</string>
|
<string name="multiplayer_opponent_score">Opponent:\n%d</string>
|
||||||
<string name="error_join_create_game">Impossible de créer ou rejoindre (Code: %d)</string>
|
<string name="error_join_create_game">Could not create or join game (Code: %d)</string>
|
||||||
<string name="error_server_connection">Échec de connexion au serveur.</string>
|
<string name="error_server_connection">Failed to connect to server.</string>
|
||||||
<string name="error_websocket_connection">Erreur de connexion WebSocket.</string>
|
<string name="error_websocket_connection">WebSocket connection error.</string>
|
||||||
<string name="error_websocket_disconnected">WebSocket déconnecté. Tentative de reconnexion…</string>
|
<string name="error_websocket_disconnected">WebSocket disconnected. Attempting to reconnect…</string>
|
||||||
<string name="error_not_your_turn">Ce n\'est pas votre tour.</string>
|
<string name="error_not_your_turn">It\'s not your turn.</string>
|
||||||
<string name="server_error_prefix">Erreur Serveur: %s</string>
|
<string name="server_error_prefix">Server Error: %s</string>
|
||||||
<string name="websocket_connected">Connecté au serveur de jeu !</string>
|
<string name="websocket_connected">Connected to game server!</string>
|
||||||
<string name="websocket_closing">Fermeture de la connexion…</string>
|
<string name="websocket_closing">Closing connection…</string>
|
||||||
<string name="websocket_closed">Connexion fermée.</string>
|
<string name="websocket_closed">Connection closed.</string>
|
||||||
|
|
||||||
<string name="game_over_draw">Égalité !</string>
|
<string name="game_over_draw">It\'s a Draw!</string>
|
||||||
<string name="game_over_won">Vous avez Gagné !</string>
|
<string name="game_over_won">You Won!</string>
|
||||||
<string name="game_over_lost">Vous avez Perdu.</string>
|
<string name="game_over_lost">You Lost.</string>
|
||||||
<string name="game_over_generic">Partie Terminée !</string>
|
<string name="game_over_generic">Game Over!</string>
|
||||||
</resources>
|
</resources>
|
@ -13,6 +13,7 @@ activity = "1.8.0"
|
|||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
gridlayout = "1.0.0"
|
gridlayout = "1.0.0"
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.9.0"
|
||||||
|
workRuntime = "2.10.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
activity-v190 = { module = "androidx.activity:activity", version.ref = "androidxActivity" }
|
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" }
|
gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "loggingInterceptor" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "loggingInterceptor" }
|
||||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
|
work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user