Refactor: Nettoyage du code et amélioration de la documentation

- Suppression des logs de débogage et commentaires superflus.
- Ajout/Amélioration des commentaires JavaDoc pour les classes et méthodes principales
  (Game, GameStats, MainActivity, OnSwipeTouchListener).
- Mise à jour des en-têtes de fichiers pour refléter le rôle actuel des classes.
- Revue générale pour la clarté et la cohérence du code.
This commit is contained in:
Augustin ROUX 2025-04-04 14:13:34 +02:00
parent b32d1e0986
commit 21ff127536
4 changed files with 463 additions and 464 deletions

View File

@ -1,114 +1,121 @@
// Fichier Game.java
// Contient la logique principale du jeu 2048 : gestion du plateau, mouvements, fusions, score.
// Cette classe est maintenant indépendante du contexte Android et de la persistance des données.
/*
Fonctions principales :
- board : Matrice 2D (int[][]) représentant la grille du jeu.
- currentScore : Suivi du score de la partie en cours.
- highestScore : Stocke le meilleur score global (reçu de l'extérieur via setHighestScore).
- addNewTile() : Ajoute une nouvelle tuile aléatoire sur une case vide.
- pushUp(), pushDown(), pushLeft(), pushRight() : Gèrent la logique de déplacement et de fusion, retournent un booléen indiquant si le plateau a changé.
- getHighestTileValue() : Retourne la valeur de la plus haute tuile sur le plateau.
- États gameWon, gameOver : Indiquent si la partie est gagnée ou terminée.
- Méthodes pour vérifier les conditions de victoire et de fin de partie.
- toString(), deserialize() : Sérialisent/désérialisent l'état essentiel du jeu (plateau, score courant) pour la sauvegarde externe.
Relations :
- MainActivity : Crée une instance de Game, appelle ses méthodes (pushX, addNewTile, getters), lui fournit le meilleur score global via setHighestScore, et utilise son état pour l'affichage et la sauvegarde.
*/
/**
* Représente la logique métier du jeu 2048. Gère l'état du plateau de jeu,
* les déplacements, les fusions, le score de la partie en cours, et les conditions
* de victoire ou de défaite. Cette classe est conçue pour être indépendante du
* framework Android (pas de dépendance au Contexte ou aux SharedPreferences).
*/
package legion.muyue.best2048;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; // Pour les méthodes de test éventuelles
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Game {
/** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
private int[][] board;
/** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
private final Random randomNumberGenerator;
/** Score de la partie actuellement en cours. */
private int currentScore = 0;
private int highestScore = 0; // Stocke le HS fourni par MainActivity
/** Meilleur score global (reçu et stocké, mais non géré logiquement ici). */
private int highestScore = 0;
/** Taille du plateau de jeu (nombre de lignes/colonnes). */
private static final int BOARD_SIZE = 4;
/** Indicateur si la condition de victoire (>= 2048) a été atteinte. */
private boolean gameWon = false;
/** Indicateur si la partie est terminée (plus de mouvements possibles). */
private boolean gameOver = false;
/**
* Constructeur pour une nouvelle partie.
* Initialise un plateau vide, définit le score à 0 et ajoute deux tuiles initiales.
* Constructeur pour démarrer une nouvelle partie.
* Initialise un plateau vide, le score à 0, et ajoute deux tuiles initiales.
*/
public Game() {
this.randomNumberGenerator = new Random();
// Le highScore sera défini par MainActivity après l'instanciation.
initializeNewBoard();
}
/**
* Constructeur utilisé lors de la restauration d'une partie sauvegardée.
* Constructeur pour restaurer une partie à partir d'un état sauvegardé.
* Le meilleur score (`highestScore`) doit être défini séparément via {@link #setHighestScore(int)}.
* Recalcule les états `gameWon` et `gameOver` en fonction du plateau fourni.
*
* @param board Le plateau de jeu restauré.
* @param score Le score courant restauré.
*/
public Game(int[][] board, int score) {
// Valider les dimensions du plateau fourni ? Pourrait être ajouté.
this.board = board;
this.currentScore = score;
this.randomNumberGenerator = new Random();
// Le highScore sera défini par MainActivity après l'instanciation.
checkWinCondition(); // Recalcule l'état basé sur le plateau chargé
checkWinCondition();
checkGameOverCondition();
}
// --- Getters / Setters ---
/**
* Retourne la valeur de la cellule aux coordonnées spécifiées.
* @param row Ligne de la cellule (0-based).
* @param column Colonne de la cellule (0-based).
* @return Valeur de la cellule, ou 0 si indices invalides (sécurité).
* Retourne la valeur de la tuile aux coordonnées spécifiées.
* @param row Ligne (0 à BOARD_SIZE-1).
* @param column Colonne (0 à BOARD_SIZE-1).
* @return Valeur de la tuile, ou 0 si les coordonnées sont invalides.
*/
public int getCellValue(int row, int column) {
if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { return 0; }
return this.board[row][column];
if (isIndexValid(row, column)) {
return this.board[row][column];
}
return 0; // Retourne 0 pour indice invalide
}
/**
* Définit la valeur de la cellule aux coordonnées spécifiées.
* @param row Ligne de la cellule (0-based).
* @param col Colonne de la cellule (0-based).
* @param value Nouvelle valeur.
* Définit la valeur d'une tuile aux coordonnées spécifiées.
* Ne fait rien si les coordonnées sont invalides.
* @param row Ligne (0 à BOARD_SIZE-1).
* @param col Colonne (0 à BOARD_SIZE-1).
* @param value Nouvelle valeur de la tuile.
*/
public void setCellValue(int row, int col, int value) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return; }
this.board[row][col] = value;
if (isIndexValid(row, col)) {
this.board[row][col] = value;
}
}
/** @return Le score actuel de la partie. */
public int getCurrentScore() { return currentScore; }
// public void setCurrentScore(int currentScore) { this.currentScore = currentScore; } // Setter interne si nécessaire
/** @return Le meilleur score connu par cet objet Game (synchronisé par MainActivity). */
/** @return Le meilleur score connu par cet objet (défini via setHighestScore). */
public int getHighestScore() { return highestScore; }
/**
* Définit le meilleur score global connu. Appelé par MainActivity après chargement
* des préférences ou après une mise à jour du score.
* @param highScore Le meilleur score connu.
* Met à jour la valeur du meilleur score stockée dans cet objet Game.
* Typiquement appelé par la classe gérant la persistance (MainActivity).
* @param highScore Le meilleur score global à stocker.
*/
public void setHighestScore(int highScore) { this.highestScore = highScore; }
/** @return L'état de victoire de la partie (true si >= 2048 atteint). */
/** @return true si une tuile 2048 (ou plus) a été atteinte, false sinon. */
public boolean isGameWon() { return gameWon; }
public void setGameWon(boolean gameWon) { this.gameWon = gameWon; }
/** @return L'état de fin de partie (true si aucun mouvement possible). */
/** @return true si aucune case n'est vide ET aucun mouvement/fusion n'est possible, false sinon. */
public boolean isGameOver() { return gameOver; }
public void setGameOver(boolean gameOver) { this.gameOver = gameOver; }
/** Met à jour la valeur de gameWon si partie gagné **/
private void setGameWon(boolean won) {this.gameWon = won;}
/** @return Une copie du plateau de jeu actuel (pour la sauvegarde externe). */
/** Met à jour la valeur de gameWon si partie gagné **/
private void setGameOver(boolean over) {this.gameOver = over;}
/**
* Retourne une copie profonde du plateau de jeu actuel.
* Utile pour la sérialisation ou pour éviter des modifications externes non désirées.
* @return Une nouvelle matrice 2D représentant l'état actuel du plateau.
*/
public int[][] getBoard() {
// Retourne une copie pour éviter modifications externes accidentelles
int[][] copy = new int[BOARD_SIZE][BOARD_SIZE];
for(int i=0; i<BOARD_SIZE; i++) {
System.arraycopy(this.board[i], 0, copy[i], 0, BOARD_SIZE);
@ -117,32 +124,27 @@ public class Game {
}
/**
* Initialise ou réinitialise le plateau pour une nouvelle partie.
* Met le score à 0 et ajoute deux tuiles aléatoires.
* ()Initialise le plateau de jeu pour une nouvelle partie.
* Remplit le plateau de zéros, réinitialise le score et les états `gameWon`/`gameOver`,
* puis ajoute deux tuiles initiales.
*/
private void initializeNewBoard() {
this.board = new int[BOARD_SIZE][BOARD_SIZE];
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Crée une nouvelle matrice vide
this.currentScore = 0;
this.gameWon = false;
this.gameOver = false;
addNewTile();
addNewTile();
addNewTile(); // Ajoute la première tuile
addNewTile(); // Ajoute la seconde tuile
}
// --- Logique du Jeu ---
/**
* Ajoute une nouvelle tuile (selon les probabilités définies)
* sur une case vide aléatoire du plateau. Ne fait rien si le plateau est plein.
* Ajoute une nouvelle tuile (2, 4, 8, etc., selon probabilités) sur une case vide aléatoire.
* Si le plateau est plein, cette méthode ne fait rien.
*/
public void addNewTile() {
if (!hasEmptyCell()) { return; }
List<int[]> emptyCells = new ArrayList<>();
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (board[row][col] == 0) { emptyCells.add(new int[]{row, col}); }
}
}
List<int[]> emptyCells = findEmptyCells();
if (!emptyCells.isEmpty()) {
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
int value = generateRandomTileValue();
@ -151,113 +153,150 @@ public class Game {
}
/**
* Génère aléatoirement la valeur d'une nouvelle tuile (2, 4, 8, etc.)
* selon des probabilités prédéfinies.
* @return La valeur de la nouvelle tuile.
* Trouve toutes les cellules vides sur le plateau.
* @return Une liste de tableaux d'entiers `[row, col]` pour chaque cellule vide.
*/
private List<int[]> findEmptyCells() {
List<int[]> emptyCells = new ArrayList<>();
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (board[row][col] == 0) {
emptyCells.add(new int[]{row, col});
}
}
}
return emptyCells;
}
/**
* Génère la valeur pour une nouvelle tuile en utilisant des probabilités prédéfinies.
* @return La valeur (2, 4, 8, ...).
*/
private int generateRandomTileValue() {
int randomValue = randomNumberGenerator.nextInt(10000);
if (randomValue < 8540) return 2; // ~85%
if (randomValue < 9740) return 4; // ~12%
if (randomValue < 9940) return 8; // ~2%
if (randomValue < 9990) return 16; // ~0.5%
// ... (autres probabilités)
if (randomValue < 9995) return 32;
if (randomValue < 9998) return 64;
if (randomValue < 9999) return 128;
return 256;
int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour pourcentages fins
if (randomValue < 8540) return 2; // 85.40%
if (randomValue < 9740) return 4; // 12.00%
if (randomValue < 9940) return 8; // 2.00%
if (randomValue < 9990) return 16; // 0.50%
if (randomValue < 9995) return 32; // 0.05%
if (randomValue < 9998) return 64; // 0.03%
if (randomValue < 9999) return 128;// 0.01%
return 256; // 0.01%
}
/**
* Tente de déplacer et fusionner les tuiles vers le HAUT.
* Met à jour le score interne en cas de fusion.
* Vérifie les conditions de victoire/défaite après le mouvement.
* @return true si au moins une tuile a bougé ou fusionné, false sinon.
* Met à jour le score interne et vérifie les états win/gameOver.
* @return true si le plateau a été modifié, false sinon.
*/
public boolean pushUp() {
boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE];
for (int col = 0; col < BOARD_SIZE; col++) {
hasMerged = new boolean[BOARD_SIZE];
for (int row = 1; row < BOARD_SIZE; row++) {
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col); int currentRow = row;
while (currentRow > 0 && getCellValue(currentRow - 1, col) == 0) { setCellValue(currentRow - 1, col, currentValue); setCellValue(currentRow, col, 0); currentRow--; boardChanged = true; }
if (currentRow > 0 && getCellValue(currentRow - 1, col) == currentValue && !hasMerged[currentRow - 1]) {
int newValue = getCellValue(currentRow - 1, col) * 2; setCellValue(currentRow - 1, col, newValue); setCellValue(currentRow, col, 0);
currentScore += newValue; hasMerged[currentRow - 1] = true; boardChanged = true;
}
}
}
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
public boolean pushUp() { return processMove(MoveDirection.UP); }
/**
* Tente de déplacer et fusionner les tuiles vers le BAS.
* @return true si changement, false sinon.
* @return true si le plateau a été modifié, false sinon.
*/
public boolean pushDown() {
boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE];
for (int col = 0; col < BOARD_SIZE; col++) {
hasMerged = new boolean[BOARD_SIZE];
for (int row = BOARD_SIZE - 2; row >= 0; row--) {
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col); int currentRow = row;
while (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == 0) { setCellValue(currentRow + 1, col, currentValue); setCellValue(currentRow, col, 0); currentRow++; boardChanged = true; }
if (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == currentValue && !hasMerged[currentRow + 1]) {
int newValue = getCellValue(currentRow + 1, col) * 2; setCellValue(currentRow + 1, col, newValue); setCellValue(currentRow, col, 0);
currentScore += newValue; hasMerged[currentRow + 1] = true; boardChanged = true;
}
}
}
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
public boolean pushDown() { return processMove(MoveDirection.DOWN); }
/**
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
* @return true si changement, false sinon.
* @return true si le plateau a été modifié, false sinon.
*/
public boolean pushLeft() {
boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE];
for (int row = 0; row < BOARD_SIZE; row++) {
hasMerged = new boolean[BOARD_SIZE];
for (int col = 1; col < BOARD_SIZE; col++) {
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col); int currentCol = col;
while (currentCol > 0 && getCellValue(row, currentCol - 1) == 0) { setCellValue(row, currentCol - 1, currentValue); setCellValue(row, currentCol, 0); currentCol--; boardChanged = true; }
if (currentCol > 0 && getCellValue(row, currentCol - 1) == currentValue && !hasMerged[currentCol - 1]) {
int newValue = getCellValue(row, currentCol - 1) * 2; setCellValue(row, currentCol - 1, newValue); setCellValue(row, currentCol, 0);
currentScore += newValue; hasMerged[currentCol - 1] = true; boardChanged = true;
}
}
}
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
public boolean pushLeft() { return processMove(MoveDirection.LEFT); }
/**
* Tente de déplacer et fusionner les tuiles vers la DROITE.
* @return true si changement, false sinon.
* @return true si le plateau a été modifié, false sinon.
*/
public boolean pushRight() {
boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE];
for (int row = 0; row < BOARD_SIZE; row++) {
hasMerged = new boolean[BOARD_SIZE];
for (int col = BOARD_SIZE - 2; col >= 0; col--) {
public boolean pushRight() { return processMove(MoveDirection.RIGHT); }
/** Énumération interne pour clarifier le traitement des mouvements. */
private enum MoveDirection { UP, DOWN, LEFT, RIGHT }
/**
* Méthode générique pour traiter un mouvement (déplacement et fusion) dans une direction donnée.
* Contient la logique de base partagée par les méthodes pushX.
* @param direction La direction du mouvement.
* @return true si le plateau a été modifié, false sinon.
*/
private boolean processMove(MoveDirection direction) {
boolean boardChanged = false;
// Itère sur l'axe perpendiculaire au mouvement
for (int i = 0; i < BOARD_SIZE; i++) {
boolean[] hasMerged = new boolean[BOARD_SIZE]; // Pour éviter double fusion sur l'axe de mouvement
// Itère sur l'axe du mouvement, dans le bon sens
int start = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? BOARD_SIZE - 2 : 1;
int end = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : BOARD_SIZE;
int step = (direction == MoveDirection.DOWN || direction == MoveDirection.RIGHT) ? -1 : 1;
for (int j = start; j != end; j += step) {
int row = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? j : i;
int col = (direction == MoveDirection.LEFT || direction == MoveDirection.RIGHT) ? j : i;
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col); int currentCol = col;
while (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == 0) { setCellValue(row, currentCol + 1, currentValue); setCellValue(row, currentCol, 0); currentCol++; boardChanged = true; }
if (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == currentValue && !hasMerged[currentCol + 1]) {
int newValue = getCellValue(row, currentCol + 1) * 2; setCellValue(row, currentCol + 1, newValue); setCellValue(row, currentCol, 0);
currentScore += newValue; hasMerged[currentCol + 1] = true; boardChanged = true;
int currentValue = getCellValue(row, col);
int currentRow = row;
int currentCol = col;
// Calcule la position cible après déplacement dans les cases vides
int targetRow = currentRow;
int targetCol = currentCol;
int nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
int nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
while (isIndexValid(nextRow, nextCol) && getCellValue(nextRow, nextCol) == 0) {
targetRow = nextRow;
targetCol = nextCol;
nextRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
nextCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
}
// Déplace la tuile si sa position cible est différente
if (targetRow != currentRow || targetCol != currentCol) {
setCellValue(targetRow, targetCol, currentValue);
setCellValue(currentRow, currentCol, 0);
boardChanged = true;
}
// Vérifie la fusion potentielle avec la case suivante dans la direction du mouvement
int mergeTargetRow = targetRow + ((direction == MoveDirection.UP) ? -1 : (direction == MoveDirection.DOWN) ? 1 : 0);
int mergeTargetCol = targetCol + ((direction == MoveDirection.LEFT) ? -1 : (direction == MoveDirection.RIGHT) ? 1 : 0);
int mergeIndex = (direction == MoveDirection.UP || direction == MoveDirection.DOWN) ? mergeTargetRow : mergeTargetCol;
if (isIndexValid(mergeTargetRow, mergeTargetCol) &&
getCellValue(mergeTargetRow, mergeTargetCol) == currentValue &&
!hasMerged[mergeIndex])
{
int newValue = currentValue * 2;
setCellValue(mergeTargetRow, mergeTargetCol, newValue);
setCellValue(targetRow, targetCol, 0); // La tuile qui fusionne disparaît
currentScore += newValue;
hasMerged[mergeIndex] = true;
boardChanged = true;
}
}
}
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
// Vérifie les conditions de fin après chaque type de mouvement complet
checkWinCondition();
checkGameOverCondition();
return boardChanged;
}
/**
* Sérialise l'état actuel du jeu (plateau et score courant) en une chaîne.
* Vérifie si les indices de ligne et colonne sont valides pour le plateau.
* @param row Ligne.
* @param col Colonne.
* @return true si les indices sont dans les limites [0, BOARD_SIZE-1].
*/
private boolean isIndexValid(int row, int col) {
return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
}
/**
* Sérialise l'état essentiel du jeu (plateau et score courant) pour sauvegarde.
* Format: "val,val,...,val,score"
* @return Chaîne sérialisée.
* @return Chaîne représentant l'état du jeu.
*/
@NonNull
@Override
@ -271,66 +310,70 @@ public class Game {
}
/**
* Crée un nouvel objet Game à partir d'une chaîne sérialisée (plateau + score).
* @param serializedState Chaîne générée par toString().
* @return Nouvel objet Game, ou null si la désérialisation échoue.
* Crée un objet Game à partir de sa représentation sérialisée (plateau + score).
* Le meilleur score doit être défini séparément après la création.
* @param serializedState La chaîne issue de {@link #toString()}.
* @return Une nouvelle instance de Game, ou null en cas d'erreur de format.
*/
public static Game deserialize(String serializedState) {
if (serializedState == null || serializedState.isEmpty()) return null;
String[] values = serializedState.split(",");
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null;
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null; // +1 pour le score
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0;
try {
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); }
}
int score = Integer.parseInt(values[index]);
int score = Integer.parseInt(values[index]); // Le dernier élément est le score
return new Game(newBoard, score);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; }
}
/**
* Vérifie si une tuile >= 2048 existe sur le plateau et met à jour l'état `gameWon`.
* Vérifie si la condition de victoire (une tuile >= 2048) est atteinte.
* Met à jour l'état interne `gameWon`.
*/
private void checkWinCondition() {
if (!gameWon) { // Optimisation: inutile de revérifier si déjà gagné
for(int r=0; r<BOARD_SIZE; r++) for(int c=0; c<BOARD_SIZE; c++) if(getCellValue(r,c)>=2048) { setGameWon(true); return; }
if (!gameWon) {
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c) >= 2048) {
setGameWon(true); return;
}
}
}
/**
* Vérifie s'il reste des mouvements possibles (case vide ou fusion adjacente).
* Met à jour l'état `gameOver`.
* Vérifie si la condition de fin de partie est atteinte (plateau plein ET aucun mouvement possible).
* Met à jour l'état interne `gameOver`.
*/
private void checkGameOverCondition() {
if (hasEmptyCell()) { setGameOver(false); return; } // Si case vide, pas game over
// Vérifie fusions adjacentes possibles
for(int r=0; r<BOARD_SIZE; r++) for(int c=0; c<BOARD_SIZE; c++) {
if (hasEmptyCell()) { setGameOver(false); return; } // Pas game over si case vide
// Vérifie s'il existe au moins une fusion possible
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) {
int current = getCellValue(r,c);
// Vérifie voisins (haut, bas, gauche, droite)
if ((r>0 && getCellValue(r-1,c)==current) || (r<BOARD_SIZE-1 && getCellValue(r+1,c)==current) ||
(c>0 && getCellValue(r,c-1)==current) || (c<BOARD_SIZE-1 && getCellValue(r,c+1)==current)) {
setGameOver(false); return; // Fusion possible, pas game over
setGameOver(false); return; // Fusion possible -> pas game over
}
}
setGameOver(true); // Aucune case vide et aucune fusion -> game over
}
/**
* @return true s'il y a au moins une case vide sur le plateau, false sinon.
* Vérifie si le plateau contient au moins une case vide (valeur 0).
* @return true si une case vide existe, false sinon.
*/
private boolean hasEmptyCell() {
for(int r=0; r<BOARD_SIZE; r++) for(int c=0; c<BOARD_SIZE; c++) if(getCellValue(r,c)==0) return true;
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (getCellValue(r,c)==0) return true;
return false;
}
/**
* Trouve et retourne la valeur de la tuile la plus élevée actuellement sur le plateau.
* @return La valeur maximale trouvée.
* Retourne la valeur de la plus haute tuile présente sur le plateau.
* @return La valeur maximale trouvée, ou 0 si le plateau est vide.
*/
public int getHighestTileValue() {
int maxTile = 0;
for(int r=0; r<BOARD_SIZE; r++) for(int c=0; c<BOARD_SIZE; c++) if(board[r][c]>maxTile) maxTile=board[r][c];
for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) if (board[r][c] > maxTile) maxTile = board[r][c];
return maxTile;
}
}

View File

@ -1,30 +1,22 @@
// Fichier GameStats.java
// Gère le stockage, le chargement et la mise à jour des statistiques du jeu 2048.
/*
Fonctions principales :
- Contient tous les champs relatifs aux statistiques (solo et multijoueur).
- loadStats(), saveStats() : Charge et sauvegarde les statistiques via SharedPreferences.
- Méthodes pour mettre à jour les statistiques : startGame(), recordMove(), recordMerge(), recordWin(), recordLoss(), endGame().
- Getters pour accéder aux valeurs des statistiques.
- formatTime() : Méthode utilitaire pour formater le temps.
Relations :
- MainActivity : Crée une instance de GameStats, l'utilise pour charger/sauvegarder les stats et met à jour les stats via ses méthodes. Récupère les valeurs via les getters pour l'affichage.
- SharedPreferences : Utilisé pour la persistance des statistiques.
*/
/**
* Gère la collecte, la persistance (via SharedPreferences) et l'accès
* aux statistiques du jeu 2048, pour les modes solo et multijoueur (si applicable).
*/
package legion.muyue.best2048;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.TimeUnit;
public class GameStats {
// Clés SharedPreferences (inchangées)
// --- Constantes pour SharedPreferences ---
private static final String PREFS_NAME = "Best2048_Prefs";
private static final String HIGH_SCORE_KEY = "high_score";
private static final String HIGH_SCORE_KEY = "high_score"; // Clé partagée avec Game/MainActivity
// Clés spécifiques aux statistiques
private static final String STATS_TOTAL_GAMES_PLAYED = "totalGamesPlayed";
// ... (autres clés inchangées) ...
private static final String STATS_TOTAL_GAMES_STARTED = "totalGamesStarted";
private static final String STATS_TOTAL_MOVES = "totalMoves";
private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs";
@ -34,6 +26,7 @@ public class GameStats {
private static final String STATS_PERFECT_GAMES = "perfectGames";
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
// ... (autres clés stats) ...
private static final String STATS_MP_GAMES_WON = "multiplayerGamesWon";
private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed";
private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak";
@ -42,20 +35,27 @@ public class GameStats {
private static final String STATS_MP_LOSSES = "totalMultiplayerLosses";
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore";
// Champs statistiques (inchangés)
// --- Champs de Statistiques ---
// Générales & Solo
private int totalGamesPlayed;
private int totalGamesStarted;
private int totalMoves;
private int currentMoves;
private long totalPlayTimeMs;
private long currentGameStartTimeMs;
private int mergesThisGame;
private int totalMerges;
private int highestTile;
private int numberOfTimesObjectiveReached;
private int perfectGames;
private int numberOfTimesObjectiveReached; // Nombre de victoires (>= 2048)
private int perfectGames; // Concept non défini ici
private long bestWinningTimeMs;
private long worstWinningTimeMs;
private int overallHighScore; // Meilleur score global
// Partie en cours (non persistées telles quelles)
private int currentMoves;
private long currentGameStartTimeMs;
private int mergesThisGame;
// Multijoueur
private int multiplayerGamesWon;
private int multiplayerGamesPlayed;
private int multiplayerBestWinningStreak;
@ -63,36 +63,39 @@ public class GameStats {
private long multiplayerTotalTimeMs;
private int totalMultiplayerLosses;
private int multiplayerHighestScore;
private int overallHighScore;
/** Contexte nécessaire pour accéder aux SharedPreferences. */
private final Context context;
/**
* Constructeur de GameStats.
* Charge immédiatement les statistiques sauvegardées via SharedPreferences.
* @param context Le contexte de l'application (nécessaire pour SharedPreferences).
* Constructeur. Initialise l'objet et charge immédiatement les statistiques
* depuis les SharedPreferences.
* @param context Contexte de l'application.
*/
public GameStats(Context context) {
this.context = context;
this.context = context.getApplicationContext(); // Utilise le contexte applicatif
loadStats();
}
// --- Persistance (SharedPreferences) ---
/**
* Charge toutes les statistiques (générales et multijoueur) et le high score global
* depuis les SharedPreferences.
* Charge toutes les statistiques persistantes depuis les SharedPreferences.
* Appelé par le constructeur.
*/
public void loadStats() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
totalGamesPlayed = prefs.getInt(STATS_TOTAL_GAMES_PLAYED, 0);
totalGamesStarted = prefs.getInt(STATS_TOTAL_GAMES_STARTED, 0);
// ... (chargement de toutes les autres clés persistantes) ...
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0);
totalMerges = prefs.getInt(STATS_TOTAL_MERGES, 0);
highestTile = prefs.getInt(STATS_HIGHEST_TILE, 0);
numberOfTimesObjectiveReached = prefs.getInt(STATS_OBJECTIVE_REACHED_COUNT, 0);
perfectGames = prefs.getInt(STATS_PERFECT_GAMES, 0);
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE);
bestWinningTimeMs = prefs.getLong(STATS_BEST_WINNING_TIME_MS, Long.MAX_VALUE); // MAX_VALUE comme défaut pour 'best'
worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0);
multiplayerGamesWon = prefs.getInt(STATS_MP_GAMES_WON, 0);
multiplayerGamesPlayed = prefs.getInt(STATS_MP_GAMES_PLAYED, 0);
@ -104,15 +107,16 @@ public class GameStats {
}
/**
* Sauvegarde toutes les statistiques (générales et multijoueur) et le high score global
* dans les SharedPreferences.
* Sauvegarde toutes les statistiques persistantes dans les SharedPreferences.
* Appelé typiquement dans `onPause` de l'activité.
*/
public void saveStats() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(HIGH_SCORE_KEY, overallHighScore);
editor.putInt(HIGH_SCORE_KEY, overallHighScore); // Sauvegarde le HS global
editor.putInt(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed);
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
// ... (sauvegarde de toutes les autres clés persistantes) ...
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs);
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
@ -128,12 +132,15 @@ public class GameStats {
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
editor.apply();
editor.apply(); // Applique les changements de manière asynchrone
}
// --- Méthodes de Mise à Jour des Statistiques ---
/**
* Met à jour les statistiques lors du démarrage d'une nouvelle partie.
* Incrémente le nombre de parties démarrées et réinitialise les compteurs de la partie en cours.
* Doit être appelée au début de chaque nouvelle partie.
* Incrémente le compteur de parties démarrées et réinitialise les stats de la partie en cours.
*/
public void startGame() {
totalGamesStarted++;
@ -143,8 +150,7 @@ public class GameStats {
}
/**
* Enregistre un mouvement effectué pendant la partie en cours.
* Incrémente le compteur de mouvements de la partie et le compteur total.
* Enregistre un mouvement réussi (qui a modifié le plateau).
*/
public void recordMove() {
currentMoves++;
@ -152,8 +158,8 @@ public class GameStats {
}
/**
* Enregistre une ou plusieurs fusions survenues pendant la partie en cours.
* @param numberOfMerges Le nombre de fusions à ajouter (idéalement précis).
* Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
* @param numberOfMerges Le nombre de fusions (si connu, sinon 1 par défaut).
*/
public void recordMerge(int numberOfMerges) {
if (numberOfMerges > 0) {
@ -163,8 +169,8 @@ public class GameStats {
}
/**
* Met à jour la statistique de la plus haute tuile atteinte si la valeur fournie est supérieure.
* @param tileValue La valeur de la tuile candidate.
* Met à jour la statistique de la plus haute tuile atteinte globalement.
* @param tileValue La valeur de la tuile la plus haute de la partie en cours.
*/
public void updateHighestTile(int tileValue) {
if (tileValue > this.highestTile) {
@ -173,27 +179,27 @@ public class GameStats {
}
/**
* Enregistre une victoire et met à jour les statistiques associées (temps, nombre de victoires).
* @param timeTakenMs Le temps mis pour gagner cette partie en millisecondes.
* Enregistre une victoire et met à jour les temps associés.
* @param timeTakenMs Temps écoulé pour cette partie gagnante.
*/
public void recordWin(long timeTakenMs) {
numberOfTimesObjectiveReached++;
endGame(timeTakenMs); // Finalise les stats de la partie
if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; }
if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; }
endGame(timeTakenMs); // Finalise aussi le temps total et parties jouées
}
/**
* Enregistre une défaite et finalise les statistiques de la partie.
* Enregistre une défaite.
*/
public void recordLoss() {
// Calcule le temps écoulé avant de finaliser
endGame(System.currentTimeMillis() - currentGameStartTimeMs);
}
/**
* Finalise les statistiques à la fin d'une partie (incrémente parties jouées, ajoute temps de jeu).
* @param timeTakenMs Le temps total de la partie qui vient de se terminer.
* Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
* @param timeTakenMs Temps total de la partie terminée.
*/
public void endGame(long timeTakenMs) {
totalGamesPlayed++;
@ -201,8 +207,9 @@ public class GameStats {
}
/**
* Ajoute une durée au temps de jeu total cumulé.
* @param durationMs Durée à ajouter en millisecondes.
* Ajoute une durée (en ms) au temps de jeu total enregistré.
* Typiquement appelé dans `onPause`.
* @param durationMs Durée à ajouter.
*/
public void addPlayTime(long durationMs) {
if (durationMs > 0) {
@ -210,13 +217,13 @@ public class GameStats {
}
}
// --- Getters ---
// --- Getters pour l'affichage ---
public int getTotalGamesPlayed() { return totalGamesPlayed; }
public int getTotalGamesStarted() { return totalGamesStarted; }
public int getTotalMoves() { return totalMoves; }
public int getCurrentMoves() { return currentMoves; }
public long getTotalPlayTimeMs() { return totalPlayTimeMs; }
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; }
public long getCurrentGameStartTimeMs() { return currentGameStartTimeMs; } // Utile pour calcul durée en cours
public int getMergesThisGame() { return mergesThisGame; }
public int getTotalMerges() { return totalMerges; }
public int getHighestTile() { return highestTile; }
@ -224,6 +231,8 @@ public class GameStats {
public int getPerfectGames() { return perfectGames; }
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
public int getOverallHighScore() { return overallHighScore; } // Getter pour HS global
// Getters Multiplayer
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
@ -231,32 +240,25 @@ public class GameStats {
public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; }
public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; }
public int getMultiplayerHighestScore() { return multiplayerHighestScore; }
public int getOverallHighScore() { return overallHighScore; } // Getter pour le HS global
// --- Setters ---
/**
* Met à jour la valeur interne du meilleur score global.
* Appelé par MainActivity pour synchroniser le high score lu depuis les préférences.
* @param highScore Le meilleur score lu.
*/
/** Met à jour la valeur interne du high score global. */
public void setHighestScore(int highScore) {
// On met à jour seulement si la valeur externe est supérieure,
// car GameStats gère aussi la mise à jour via les scores des parties.
// Ou plus simplement, on fait confiance à la valeur lue par MainActivity.
this.overallHighScore = highScore;
// Met à jour si la nouvelle valeur est meilleure
if (highScore > this.overallHighScore) {
this.overallHighScore = highScore;
// La sauvegarde se fait via saveStats() globalement
}
}
/** Définit le timestamp de début de la partie en cours. */
public void setCurrentGameStartTimeMs(long timeMs) { this.currentGameStartTimeMs = timeMs; }
/**
* Définit le timestamp de démarrage pour la partie en cours.
* @param timeMs Timestamp en millisecondes.
*/
public void setCurrentGameStartTimeMs(long timeMs) {
this.currentGameStartTimeMs = timeMs;
}
// --- Méthodes calculées ---
// --- Méthodes Calculées ---
/** @return Temps moyen par partie terminée en millisecondes. */
public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; }
/** @return Score moyen par partie multijoueur terminée. */
public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; }
/** @return Temps moyen par partie multijoueur terminée en millisecondes. */
public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; }
/**
@ -264,6 +266,7 @@ public class GameStats {
* @param milliseconds Durée en millisecondes.
* @return Chaîne de temps formatée.
*/
@SuppressLint("DefaultLocale")
public static String formatTime(long milliseconds) {
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60;

View File

@ -1,32 +1,14 @@
// Fichier MainActivity.java
// Activité principale de l'application 2048, gère l'interface utilisateur et la coordination du jeu.
/*
Fonctions principales :
- findViews() : Récupère les références des éléments UI du layout XML.
- initializeGameAndStats() : Crée les instances de Game et GameStats, charge les données sauvegardées.
- setupListeners() : Attache les listeners aux boutons et au plateau de jeu (pour les swipes).
- updateUI(), updateBoard(), updateScores() : Met à jour l'affichage du jeu (plateau, scores).
- setTileStyle() : Applique le style visuel à une tuile individuelle.
- handleSwipe() : Traite un geste de swipe, appelle la logique de Game, met à jour les stats et l'UI.
- startNewGame() : Initialise une nouvelle partie.
- showRestartConfirmationDialog(), showGameWonDialog(), showGameOverDialog(), showMenu(), showMultiplayerScreen() : Affiche diverses boîtes de dialogue.
- toggleStatistics(), updateStatisticsTextViews() : Gère l'affichage et la mise à jour du panneau de statistiques (via ViewStub).
- Cycle de vie : onCreate(), onResume(), onPause() pour initialiser, reprendre le timer, et sauvegarder l'état/stats.
- Persistance : Utilise SharedPreferences pour sauvegarder/charger l'état du jeu (via Game.toString/deserialize) et le meilleur score. Les stats détaillées sont gérées par GameStats.
Relations :
- Game : Instance contenant la logique pure du jeu (plateau, score, mouvements).
- GameStats : Instance contenant la logique et les données des statistiques.
- OnSwipeTouchListener : Détecte les swipes sur le plateau.
- Layout XML (activity_main.xml, stats_layout.xml, etc.) : Définit la structure de l'UI.
- Ressources (strings, colors, dimens, styles, drawables) : Utilisées pour l'apparence de l'UI.
- SharedPreferences : Pour la sauvegarde des données persistantes.
*/
/**
* Activité principale de l'application 2048.
* Gère l'interface utilisateur (plateau, scores, boutons), coordonne les interactions
* avec la logique du jeu (classe Game) et la gestion des statistiques (classe GameStats).
* Gère également le cycle de vie de l'application et la persistance de l'état du jeu.
*/
package legion.muyue.best2048;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.TypedValue;
@ -46,7 +28,7 @@ import android.widget.Button;
public class MainActivity extends AppCompatActivity {
// --- UI Elements ---
private GridLayout boardGridLayout;
private TextView currentScoreTextView;
private TextView highestScoreTextView;
@ -55,25 +37,25 @@ public class MainActivity extends AppCompatActivity {
private Button statisticsButton;
private Button menuButton;
private ViewStub statisticsViewStub;
private View inflatedStatsView; // Référence à la vue des stats une fois gonflée
private View inflatedStatsView;
// --- Game Logic & Stats ---
private Game game;
private GameStats gameStats; // Instance pour gérer les stats
private GameStats gameStats;
private static final int BOARD_SIZE = 4;
// --- State Management ---
private boolean statisticsVisible = false;
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } // Nouvel état de jeu
private GameFlowState currentGameState = GameFlowState.PLAYING; // Initialisation
// --- Preferences ---
private boolean statisticsVisible = false;
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER }
private GameFlowState currentGameState = GameFlowState.PLAYING;
private SharedPreferences preferences;
private static final String PREFS_NAME = "Best2048_Prefs";
private static final String HIGH_SCORE_KEY = "high_score";
private static final String GAME_STATE_KEY = "game_state";
// --- Activity Lifecycle ---
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -81,26 +63,25 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews(); // Récupère les vues
initializeGameAndStats(); // Initialise Game, GameStats et charge les données
setupListeners(); // Attache les listeners
findViews();
initializeGameAndStats();
setupListeners();
}
@Override
protected void onResume() {
super.onResume();
// Redémarre le chrono pour la partie en cours SI elle n'est pas finie
if (game != null && gameStats != null && !game.isGameOver() && !game.isGameWon()) {
gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis()); // Utilise setter de GameStats
gameStats.setCurrentGameStartTimeMs(System.currentTimeMillis());
}
// Gère le réaffichage potentiel des stats si l'activité reprend
if (statisticsVisible) {
if (inflatedStatsView != null) { // Si déjà gonflé
updateStatisticsTextViews(); // Met à jour les données affichées
if (inflatedStatsView != null) {
updateStatisticsTextViews();
inflatedStatsView.setVisibility(View.VISIBLE);
multiplayerButton.setVisibility(View.GONE);
} else {
// Si pas encore gonflé (cas rare mais possible), on le fait afficher
toggleStatistics();
}
}
@ -109,18 +90,18 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onPause() {
super.onPause();
// Sauvegarde l'état et les stats si le jeu existe
if (game != null && gameStats != null) {
// Met à jour le temps total SI la partie n'est pas terminée
if (!game.isGameOver() && !game.isGameWon()) {
gameStats.addPlayTime(System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs()); // Utilise méthode GameStats
gameStats.addPlayTime(System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs());
}
saveGame(); // Sauvegarde l'état du jeu (plateau + score courant) et le HS
gameStats.saveStats(); // Sauvegarde toutes les stats via GameStats
saveGame();
gameStats.saveStats();
}
}
// --- Initialisation ---
/**
* Récupère les références des vues du layout principal via leur ID.
@ -144,20 +125,20 @@ public class MainActivity extends AppCompatActivity {
private void initializeGameAndStats() {
preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
gameStats = new GameStats(this);
loadGame(); // Charge jeu et met à jour high score
loadGame();
updateUI();
if (game == null) {
startNewGame(); // Assure une partie valide si chargement échoue
startNewGame();
} else {
// Détermine l'état initial basé sur le jeu chargé
if (game.isGameOver()) {
currentGameState = GameFlowState.GAME_OVER;
} else if (game.isGameWon()) {
// Si on charge une partie déjà gagnée, on considère qu'on a déjà vu la dialog
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
} else {
currentGameState = GameFlowState.PLAYING;
// Redémarre le timer dans onResume
}
}
}
@ -172,20 +153,20 @@ public class MainActivity extends AppCompatActivity {
});
statisticsButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
toggleStatistics(); // Affiche/masque les stats
toggleStatistics();
});
menuButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
showMenu(); // Affiche dialogue placeholder
showMenu();
});
multiplayerButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
showMultiplayerScreen(); // Affiche dialogue placeholder
showMultiplayerScreen();
});
setupSwipeListener(); // Attache le listener de swipe
setupSwipeListener();
}
// --- Mise à jour UI ---
/**
* Met à jour complètement l'interface utilisateur (plateau et scores).
@ -205,16 +186,16 @@ public class MainActivity extends AppCompatActivity {
for (int col = 0; col < BOARD_SIZE; col++) {
TextView tileTextView = new TextView(this);
int value = game.getCellValue(row, col);
setTileStyle(tileTextView, value); // Applique le style
// Définit les LayoutParams pour que la tuile remplisse la cellule du GridLayout
setTileStyle(tileTextView, value);
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.width = 0; params.height = 0; // Poids gère la taille
params.rowSpec = GridLayout.spec(row, 1f); // Prend 1 fraction de l'espace en hauteur
params.columnSpec = GridLayout.spec(col, 1f); // Prend 1 fraction de l'espace en largeur
params.width = 0; params.height = 0;
params.rowSpec = GridLayout.spec(row, 1f);
params.columnSpec = GridLayout.spec(col, 1f);
int margin = (int) getResources().getDimension(R.dimen.tile_margin);
params.setMargins(margin, margin, margin, margin); // Applique les marges
params.setMargins(margin, margin, margin, margin);
tileTextView.setLayoutParams(params);
boardGridLayout.addView(tileTextView); // Ajoute la tuile au GridLayout
boardGridLayout.addView(tileTextView);
}
}
}
@ -233,7 +214,7 @@ public class MainActivity extends AppCompatActivity {
* @param value La valeur numérique de la tuile (0 pour vide).
*/
private void setTileStyle(TextView tileTextView, int value) {
// Code de styling (inchangé par rapport à la correction précédente)
tileTextView.setText(value > 0 ? String.valueOf(value) : "");
tileTextView.setGravity(Gravity.CENTER);
tileTextView.setTypeface(null, android.graphics.Typeface.BOLD);
@ -260,7 +241,7 @@ public class MainActivity extends AppCompatActivity {
}
// --- Gestion des Actions Utilisateur ---
/**
* Configure le listener pour détecter les swipes sur le plateau de jeu.
@ -289,20 +270,19 @@ public class MainActivity extends AppCompatActivity {
* @param direction La direction du swipe détecté (UP, DOWN, LEFT, RIGHT).
*/
private void handleSwipe(Direction direction) {
// Si le jeu n'est pas initialisé ou s'il est DÉJÀ terminé, ignorer le swipe.
if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
return;
}
// Stocker le score avant le mouvement pour calculer le delta
int scoreBefore = game.getCurrentScore();
// Indique si le mouvement a effectivement changé l'état du plateau
boolean boardChanged = false;
// --- 1. Tenter d'effectuer le mouvement ---
// Les méthodes pushX() de l'objet Game contiennent la logique de déplacement/fusion
// et appellent en interne checkWinCondition() et checkGameOverCondition()
// pour mettre à jour les états isGameWon() et isGameOver().
switch (direction) {
case UP:
boardChanged = game.pushUp();
@ -318,63 +298,63 @@ public class MainActivity extends AppCompatActivity {
break;
}
// --- 2. Traiter les conséquences SI le plateau a changé ---
if (boardChanged) {
// Mettre à jour les statistiques liées au mouvement réussi
gameStats.recordMove();
int scoreAfter = game.getCurrentScore();
int scoreDelta = scoreAfter - scoreBefore;
if (scoreDelta > 0) {
// Supposition simpliste : une augmentation de score implique au moins une fusion
gameStats.recordMerge(1);
// Vérifier et mettre à jour le meilleur score si nécessaire
if (scoreAfter > game.getHighestScore()) {
game.setHighestScore(scoreAfter); // Met à jour dans l'objet Game
gameStats.setHighestScore(scoreAfter); // Met à jour et sauvegarde dans GameStats
game.setHighestScore(scoreAfter);
gameStats.setHighestScore(scoreAfter);
}
}
// Mettre à jour la tuile la plus haute atteinte
gameStats.updateHighestTile(game.getHighestTileValue());
// Ajouter une nouvelle tuile aléatoire sur le plateau
game.addNewTile();
// Mettre à jour l'affichage complet du plateau et des scores
updateUI();
}
// --- 3. Vérifier l'état final du jeu (Gagné / Perdu) ---
// Cette vérification est faite APRÈS la tentative de mouvement,
// On vérifie aussi qu'on n'a pas DÉJÀ traité la fin de partie dans ce même appel.
if (currentGameState != GameFlowState.GAME_OVER) {
// a) Condition de Victoire (atteindre 2048 ou plus)
// On vérifie aussi qu'on était en train de jouer normalement (pas déjà gagné et décidé de continuer)
if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) {
currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Mettre à jour l'état de flux
// Enregistrer les statistiques de victoire
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordWin(timeTaken);
// Afficher la boîte de dialogue de victoire
showGameWonKeepPlayingDialog();
// b) Condition de Défaite (Game Over - pas de case vide ET pas de fusion possible)
// Cette condition est vérifiée seulement si on n'a pas déjà gagné.
} else if (game.isGameOver()) {
currentGameState = GameFlowState.GAME_OVER; // Mettre à jour l'état de flux
// Enregistrer les statistiques de défaite et finaliser la partie
currentGameState = GameFlowState.GAME_OVER;
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordLoss();
gameStats.endGame(timeTaken); // Finalise le temps, etc.
// Afficher la boîte de dialogue de Game Over
gameStats.endGame(timeTaken);
showGameOverDialog();
if (!boardChanged) {
updateUI(); // Assure que le score final affiché est correct.
updateUI();
}
}
// c) Ni gagné, ni perdu : Le jeu continue. L'état reste PLAYING ou WON_DIALOG_SHOWN.
}
}
@ -396,11 +376,11 @@ public class MainActivity extends AppCompatActivity {
* crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI.
*/
private void startNewGame() {
gameStats.startGame(); // Réinitialise stats de partie
game = new Game(); // Crée un nouveau jeu
game.setHighestScore(gameStats.getOverallHighScore()); // Applique HS global
currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER
updateUI(); // Met à jour affichage
gameStats.startGame();
game = new Game();
game.setHighestScore(gameStats.getOverallHighScore());
currentGameState = GameFlowState.PLAYING;
updateUI();
}
/**
@ -410,24 +390,24 @@ public class MainActivity extends AppCompatActivity {
private void showGameWonKeepPlayingDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_game_won, null); // Gonfle le layout personnalisé
View dialogView = inflater.inflate(R.layout.dialog_game_won, null);
builder.setView(dialogView);
builder.setCancelable(false); // Empêche de fermer sans choisir
builder.setCancelable(false);
// Récupère les boutons DANS la vue gonflée
Button keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton);
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon);
final AlertDialog dialog = builder.create();
keepPlayingButton.setOnClickListener(v -> {
// L'état est déjà WON_DIALOG_SHOWN, on ne fait rien de spécial, le jeu continue.
dialog.dismiss();
});
newGameButton.setOnClickListener(v -> {
dialog.dismiss();
startNewGame(); // Démarre une nouvelle partie
startNewGame();
});
dialog.show();
@ -440,35 +420,35 @@ public class MainActivity extends AppCompatActivity {
private void showGameOverDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_game_over, null); // Gonfle le layout personnalisé
View dialogView = inflater.inflate(R.layout.dialog_game_over, null);
builder.setView(dialogView);
builder.setCancelable(false); // Empêche de fermer sans choisir
builder.setCancelable(false);
// Récupère les vues DANS la vue gonflée
TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver);
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver);
Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver);
// Met à jour le message avec le score final
messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore()));
final AlertDialog dialog = builder.create();
newGameButton.setOnClickListener(v -> {
dialog.dismiss();
startNewGame(); // Démarre une nouvelle partie
startNewGame();
});
quitButton.setOnClickListener(v -> {
dialog.dismiss();
finish(); // Ferme l'application
finish();
});
dialog.show();
}
// --- Gestion des Statistiques (UI) ---
/**
* Affiche ou masque le panneau de statistiques.
@ -477,20 +457,20 @@ public class MainActivity extends AppCompatActivity {
private void toggleStatistics() {
statisticsVisible = !statisticsVisible;
if (statisticsVisible) {
if (inflatedStatsView == null) { // Gonfle si pas encore fait
if (inflatedStatsView == null) {
inflatedStatsView = statisticsViewStub.inflate();
// Attache listener au bouton Back une fois la vue gonflée
Button backButton = inflatedStatsView.findViewById(R.id.backButton);
backButton.setOnClickListener(v -> toggleStatistics()); // Recliquer sur Back re-appelle toggle
backButton.setOnClickListener(v -> toggleStatistics());
}
updateStatisticsTextViews(); // Remplit les champs avec les données actuelles
inflatedStatsView.setVisibility(View.VISIBLE); // Affiche
multiplayerButton.setVisibility(View.GONE); // Masque bouton multi
updateStatisticsTextViews();
inflatedStatsView.setVisibility(View.VISIBLE);
multiplayerButton.setVisibility(View.GONE);
} else {
if (inflatedStatsView != null) { // Masque si la vue existe
if (inflatedStatsView != null) {
inflatedStatsView.setVisibility(View.GONE);
}
multiplayerButton.setVisibility(View.VISIBLE); // Réaffiche bouton multi
multiplayerButton.setVisibility(View.VISIBLE);
}
}
@ -501,7 +481,7 @@ public class MainActivity extends AppCompatActivity {
private void updateStatisticsTextViews() {
if (inflatedStatsView == null || gameStats == null) return;
// Récupération des TextViews dans la vue gonflée (idem)
TextView highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label);
TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label);
TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_label);
@ -522,12 +502,12 @@ public class MainActivity extends AppCompatActivity {
TextView multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label);
TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label);
TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_label);
TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label); // ID toujours potentiellement dupliqué
TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label);
TextView totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label);
TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label);
TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game);
// MAJ textes avec getters de gameStats
highScoreStatsLabel.setText(getString(R.string.high_score_stats, gameStats.getOverallHighScore()));
totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed()));
totalGamesStartedLabel.setText(getString(R.string.total_games_started, gameStats.getTotalGamesStarted()));
@ -545,13 +525,13 @@ public class MainActivity extends AppCompatActivity {
totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, gameStats.getTotalMultiplayerLosses()));
multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, gameStats.getMultiplayerHighestScore()));
// Calculs Pourcentages
String winPercentage = (gameStats.getTotalGamesStarted() > 0) ? String.format("%.2f%%", ((double) gameStats.getNumberOfTimesObjectiveReached() / gameStats.getTotalGamesStarted()) * 100) : "N/A";
winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage));
String multiplayerWinRate = (gameStats.getMultiplayerGamesPlayed() > 0) ? String.format("%.2f%%", ((double) gameStats.getMultiplayerGamesWon() / gameStats.getMultiplayerGamesPlayed()) * 100) : "N/A";
multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate));
// Calculs Temps
totalPlayTimeLabel.setText(getString(R.string.total_play_time, GameStats.formatTime(gameStats.getTotalPlayTimeMs())));
long currentDuration = (game != null && !game.isGameOver() && !game.isGameWon() && gameStats != null) ? System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs() : 0;
currentGameTimeLabel.setText(getString(R.string.current_game_time, GameStats.formatTime(currentDuration)));
@ -561,32 +541,32 @@ public class MainActivity extends AppCompatActivity {
worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (gameStats.getWorstWinningTimeMs() != 0) ? GameStats.formatTime(gameStats.getWorstWinningTimeMs()) : "N/A"));
}
// --- Dialogues / Placeholders ---
/** Affiche un dialogue placeholder pour le menu. */
private void showMenu() { /* ... (inchangé) ... */
private void showMenu() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Menu").setMessage("Fonctionnalité de menu à venir !").setPositiveButton("OK", null);
builder.create().show();
}
/** Affiche un dialogue placeholder pour le multijoueur. */
private void showMultiplayerScreen() { /* ... (inchangé) ... */
private void showMultiplayerScreen() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null);
builder.create().show();
}
// --- Sauvegarde / Chargement ---
/** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */
private void saveGame() {
SharedPreferences.Editor editor = preferences.edit();
if (game != null) {
editor.putString(GAME_STATE_KEY, game.toString()); // Sérialise Game (plateau + score courant)
editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); // Sauvegarde le HS contenu dans Game
editor.putString(GAME_STATE_KEY, game.toString());
editor.putInt(HIGH_SCORE_KEY, game.getHighestScore());
} else {
editor.remove(GAME_STATE_KEY); // Optionnel: nettoyer si pas de jeu
editor.remove(GAME_STATE_KEY);
}
editor.apply();
}
@ -594,28 +574,27 @@ public class MainActivity extends AppCompatActivity {
/** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */
private void loadGame() {
String gameStateString = preferences.getString(GAME_STATE_KEY, null);
int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0); // HS lu depuis prefs
int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0);
if (gameStats != null) { // S'assure que GameStats a le HS correct
if (gameStats != null) {
gameStats.setHighestScore(savedHighScore);
}
if (gameStateString != null) {
game = Game.deserialize(gameStateString);
if (game != null) {
game.setHighestScore(savedHighScore); // Applique HS à Game
// Détermine l'état basé sur le jeu chargé
if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER;
else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Si gagné avant, on continue
else currentGameState = GameFlowState.PLAYING;
} else { game = null; } // Échec désérialisation
} else { game = null; } // Pas de sauvegarde
game.setHighestScore(savedHighScore);
if (game == null) { // Si pas de jeu chargé ou erreur
if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER;
else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN;
else currentGameState = GameFlowState.PLAYING;
} else { game = null; }
} else { game = null; }
if (game == null) {
game = new Game();
game.setHighestScore(savedHighScore);
currentGameState = GameFlowState.PLAYING;
// Pas besoin d'appeler gameStats.startGame() ici, sera fait dans initializeGame OU startNewGame si nécessaire
}
}

View File

@ -1,151 +1,125 @@
// Fichier OnSwipeTouchListener.java
// Classe utilitaire pour détecter les gestes de balayage (swipe).
/*
Fonctions principales :
- Utilise GestureDetector pour détecter les swipes.
- Définit une interface SwipeListener pour notifier la classe appelante (MainActivity).
- onFling() : Détecte la direction du swipe (haut, bas, gauche, droite).
- SWIPE_THRESHOLD, SWIPE_VELOCITY_THRESHOLD : Constantes pour la sensibilité du swipe.
Relations :
- MainActivity : MainActivity crée une instance et est notifiée des swipes via l'interface SwipeListener.
*/
/**
* Listener de vue personnalisé qui détecte les gestes de balayage (swipe)
* dans les quatre directions cardinales et notifie un listener externe.
* Utilise {@link GestureDetector} pour l'analyse des gestes.
*/
package legion.muyue.best2048;
import android.annotation.SuppressLint; // Ajout
import android.annotation.SuppressLint;
import android.content.Context;
// import android.location.GnssAntennaInfo; // Import inutile trouvé dans le dump
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull; // Ajout
import androidx.annotation.NonNull;
public class OnSwipeTouchListener implements View.OnTouchListener {
/** Détecteur de gestes standard d'Android. */
private final GestureDetector gestureDetector;
/** Listener externe à notifier lors de la détection d'un swipe. */
private final SwipeListener listener;
/**
* Interface pour les événements de swipe. Toute classe qui veut réagir aux swipes
* doit implémenter cette interface et passer une instance à OnSwipeTouchListener.
* Interface à implémenter par les classes souhaitant réagir aux événements de swipe.
*/
public interface SwipeListener {
/**
* Appelée lorsqu'un swipe vers le haut est détecté.
*/
/** Appelée lorsqu'un swipe vers le haut est détecté. */
void onSwipeTop();
/**
* Appelée lorsqu'un swipe vers le bas est détecté.
*/
/** Appelée lorsqu'un swipe vers le bas est détecté. */
void onSwipeBottom();
/**
* Appelée lorsqu'un swipe vers la gauche est détecté.
*/
/** Appelée lorsqu'un swipe vers la gauche est détecté. */
void onSwipeLeft();
/**
* Appelée lorsqu'un swipe vers la droite est détecté.
*/
/** Appelée lorsqu'un swipe vers la droite est détecté. */
void onSwipeRight();
}
/**
* Constructeur de la classe OnSwipeTouchListener.
*
* @param ctx Le contexte de l'application. Nécessaire pour GestureDetector.
* @param listener L'instance qui écoutera les événements de swipe.
* Constructeur.
* @param context Contexte applicatif, nécessaire pour `GestureDetector`.
* @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null.
*/
public OnSwipeTouchListener(Context ctx, SwipeListener listener) {
gestureDetector = new GestureDetector(ctx, new GestureListener());
public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) {
this.gestureDetector = new GestureDetector(context, new GestureListener());
this.listener = listener;
}
/**
* Méthode appelée lorsqu'un événement tactile se produit sur la vue attachée.
* Elle transmet l'événement au GestureDetector pour analyse.
*
* @param v La vue sur laquelle l'événement tactile s'est produit.
* @param event L'objet MotionEvent décrivant l'événement tactile.
* @return true si l'événement a été géré, false sinon.
* Intercepte les événements tactiles sur la vue associée et les délègue
* au {@link GestureDetector} pour analyse.
* @param v La vue touchée.
* @param event L'événement tactile.
* @return true si le geste a été consommé par le détecteur, false sinon.
*/
@SuppressLint("ClickableViewAccessibility") // Ajout
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
// Passe l'événement au GestureDetector. Si ce dernier le gère (ex: détecte un onFling),
// il retournera true, et l'événement ne sera pas propagé davantage.
return gestureDetector.onTouchEvent(event);
}
/**
* Classe interne qui étend GestureDetector.SimpleOnGestureListener pour gérer
* spécifiquement les gestes de balayage (fling).
* Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide).
*/
private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_THRESHOLD = 100; // Distance minimale du swipe (en pixels).
private static final int SWIPE_VELOCITY_THRESHOLD = 100; // Vitesse minimale du swipe (en pixels par seconde).
/** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */
private static final int SWIPE_THRESHOLD = 100;
/** Vitesse minimale (en pixels/sec) pour qu'un mouvement soit considéré comme un swipe. */
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
/**
* Méthode appelée lorsqu'un appui initial sur l'écran est détecté.
* On retourne toujours 'true' pour indiquer qu'on gère cet événement.
*
* @param e L'objet MotionEvent décrivant l'appui initial.
* @return true, car on gère toujours l'événement 'onDown'.
* Toujours retourner true pour onDown garantit que les événements suivants
* (comme onFling) seront bien reçus par ce listener.
*/
@Override
public boolean onDown(@NonNull MotionEvent e) { // Ajout @NonNull
public boolean onDown(@NonNull MotionEvent e) {
return true;
}
/**
* Méthode appelée lorsqu'un geste de balayage (fling) est détecté.
*
* @param e1 L'objet MotionEvent du premier appui (début du swipe). Peut être null.
* @param e2 L'objet MotionEvent de la fin du swipe.
* @param velocityX La vitesse du swipe en pixels par seconde sur l'axe X.
* @param velocityY La vitesse du swipe en pixels par seconde sur l'axe Y.
* @return true si le geste de balayage a été géré, false sinon.
* Appelée quand un geste de 'fling' (balayage rapide) est détecté.
* Analyse la direction et la vitesse pour déterminer s'il s'agit d'un swipe valide
* et notifie le {@link SwipeListener} externe.
*/
@Override
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { // Ajout @NonNull
// Vérification de nullité pour e1, nécessaire car onFling peut être appelé même si onDown retourne false ou si le geste est complexe.
if (e1 == null) {
return false;
}
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
if (e1 == null) return false; // Point de départ est nécessaire
boolean result = false;
try {
float diffY = e2.getY() - e1.getY(); // Différence de position sur l'axe Y.
float diffX = e2.getX() - e1.getX(); // Différence de position sur l'axe X.
float diffY = e2.getY() - e1.getY();
float diffX = e2.getX() - e1.getX();
// Détermine si le swipe est plutôt horizontal ou vertical.
// Priorité au mouvement le plus ample (horizontal ou vertical)
if (Math.abs(diffX) > Math.abs(diffY)) {
// Swipe horizontal.
// Mouvement principalement horizontal
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
listener.onSwipeRight(); // Swipe vers la droite.
listener.onSwipeRight();
} else {
listener.onSwipeLeft(); // Swipe vers la gauche.
listener.onSwipeLeft();
}
result = true;
result = true; // Geste horizontal traité
}
} else {
// Swipe vertical.
// Mouvement principalement vertical
if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
if (diffY > 0) {
listener.onSwipeBottom(); // Swipe vers le bas.
listener.onSwipeBottom();
} else {
listener.onSwipeTop(); // Swipe vers le haut.
listener.onSwipeTop();
}
result = true;
result = true; // Geste vertical traité
}
}
} catch (Exception exception) {
exception.fillInStackTrace(); // Gestion des erreurs (journalisation).
// 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.
}
return result;
}
}
}
} // Fin OnSwipeTouchListener