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 // 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. * 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
Fonctions principales : * de victoire ou de défaite. Cette classe est conçue pour être indépendante du
- board : Matrice 2D (int[][]) représentant la grille du jeu. * framework Android (pas de dépendance au Contexte ou aux SharedPreferences).
- 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.
*/ */
package legion.muyue.best2048; package legion.muyue.best2048;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; // Pour les méthodes de test éventuelles
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
public class Game { public class Game {
/** Le plateau de jeu, une matrice 2D d'entiers. 0 représente une case vide. */
private int[][] board; private int[][] board;
/** Générateur de nombres aléatoires pour l'ajout de nouvelles tuiles. */
private final Random randomNumberGenerator; private final Random randomNumberGenerator;
/** Score de la partie actuellement en cours. */
private int currentScore = 0; 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; private static final int BOARD_SIZE = 4;
/** Indicateur si la condition de victoire (>= 2048) a été atteinte. */
private boolean gameWon = false; private boolean gameWon = false;
/** Indicateur si la partie est terminée (plus de mouvements possibles). */
private boolean gameOver = false; private boolean gameOver = false;
/** /**
* Constructeur pour une nouvelle partie. * Constructeur pour démarrer une nouvelle partie.
* Initialise un plateau vide, définit le score à 0 et ajoute deux tuiles initiales. * Initialise un plateau vide, le score à 0, et ajoute deux tuiles initiales.
*/ */
public Game() { public Game() {
this.randomNumberGenerator = new Random(); this.randomNumberGenerator = new Random();
// Le highScore sera défini par MainActivity après l'instanciation.
initializeNewBoard(); 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 board Le plateau de jeu restauré.
* @param score Le score courant restauré. * @param score Le score courant restauré.
*/ */
public Game(int[][] board, int score) { public Game(int[][] board, int score) {
// Valider les dimensions du plateau fourni ? Pourrait être ajouté.
this.board = board; this.board = board;
this.currentScore = score; this.currentScore = score;
this.randomNumberGenerator = new Random(); this.randomNumberGenerator = new Random();
// Le highScore sera défini par MainActivity après l'instanciation. checkWinCondition();
checkWinCondition(); // Recalcule l'état basé sur le plateau chargé
checkGameOverCondition(); checkGameOverCondition();
} }
// --- Getters / Setters --- // --- Getters / Setters ---
/** /**
* Retourne la valeur de la cellule aux coordonnées spécifiées. * Retourne la valeur de la tuile aux coordonnées spécifiées.
* @param row Ligne de la cellule (0-based). * @param row Ligne (0 à BOARD_SIZE-1).
* @param column Colonne de la cellule (0-based). * @param column Colonne (0 à BOARD_SIZE-1).
* @return Valeur de la cellule, ou 0 si indices invalides (sécurité). * @return Valeur de la tuile, ou 0 si les coordonnées sont invalides.
*/ */
public int getCellValue(int row, int column) { public int getCellValue(int row, int column) {
if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { return 0; } if (isIndexValid(row, column)) {
return this.board[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. * Définit la valeur d'une tuile aux coordonnées spécifiées.
* @param row Ligne de la cellule (0-based). * Ne fait rien si les coordonnées sont invalides.
* @param col Colonne de la cellule (0-based). * @param row Ligne (0 à BOARD_SIZE-1).
* @param value Nouvelle valeur. * @param col Colonne (0 à BOARD_SIZE-1).
* @param value Nouvelle valeur de la tuile.
*/ */
public void setCellValue(int row, int col, int value) { public void setCellValue(int row, int col, int value) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return; } if (isIndexValid(row, col)) {
this.board[row][col] = value; this.board[row][col] = value;
} }
}
/** @return Le score actuel de la partie. */ /** @return Le score actuel de la partie. */
public int getCurrentScore() { return currentScore; } 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 (défini via setHighestScore). */
/** @return Le meilleur score connu par cet objet Game (synchronisé par MainActivity). */
public int getHighestScore() { return highestScore; } public int getHighestScore() { return highestScore; }
/** /**
* Définit le meilleur score global connu. Appelé par MainActivity après chargement * Met à jour la valeur du meilleur score stockée dans cet objet Game.
* des préférences ou après une mise à jour du score. * Typiquement appelé par la classe gérant la persistance (MainActivity).
* @param highScore Le meilleur score connu. * @param highScore Le meilleur score global à stocker.
*/ */
public void setHighestScore(int highScore) { this.highestScore = highScore; } 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 boolean isGameWon() { return gameWon; }
public void setGameWon(boolean gameWon) { this.gameWon = gameWon; } /** @return true si aucune case n'est vide ET aucun mouvement/fusion n'est possible, false sinon. */
/** @return L'état de fin de partie (true si aucun mouvement possible). */
public boolean isGameOver() { return gameOver; } 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() { public int[][] getBoard() {
// Retourne une copie pour éviter modifications externes accidentelles
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(this.board[i], 0, copy[i], 0, BOARD_SIZE); 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. * ()Initialise le plateau de jeu pour une nouvelle partie.
* Met le score à 0 et ajoute deux tuiles aléatoires. * Remplit le plateau de zéros, réinitialise le score et les états `gameWon`/`gameOver`,
* puis ajoute deux tuiles initiales.
*/ */
private void initializeNewBoard() { 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.currentScore = 0;
this.gameWon = false; this.gameWon = false;
this.gameOver = false; this.gameOver = false;
addNewTile(); addNewTile(); // Ajoute la première tuile
addNewTile(); addNewTile(); // Ajoute la seconde tuile
} }
// --- Logique du Jeu --- // --- Logique du Jeu ---
/** /**
* Ajoute une nouvelle tuile (selon les probabilités définies) * Ajoute une nouvelle tuile (2, 4, 8, etc., selon probabilités) sur une case vide aléatoire.
* sur une case vide aléatoire du plateau. Ne fait rien si le plateau est plein. * Si le plateau est plein, cette méthode ne fait rien.
*/ */
public void addNewTile() { public void addNewTile() {
if (!hasEmptyCell()) { return; } List<int[]> emptyCells = 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}); }
}
}
if (!emptyCells.isEmpty()) { if (!emptyCells.isEmpty()) {
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size())); int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
int value = generateRandomTileValue(); 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.) * Trouve toutes les cellules vides sur le plateau.
* selon des probabilités prédéfinies. * @return Une liste de tableaux d'entiers `[row, col]` pour chaque cellule vide.
* @return La valeur de la nouvelle tuile. */
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() { private int generateRandomTileValue() {
int randomValue = randomNumberGenerator.nextInt(10000); int randomValue = randomNumberGenerator.nextInt(10000); // Base 10000 pour pourcentages fins
if (randomValue < 8540) return 2; // ~85% if (randomValue < 8540) return 2; // 85.40%
if (randomValue < 9740) return 4; // ~12% if (randomValue < 9740) return 4; // 12.00%
if (randomValue < 9940) return 8; // ~2% if (randomValue < 9940) return 8; // 2.00%
if (randomValue < 9990) return 16; // ~0.5% if (randomValue < 9990) return 16; // 0.50%
// ... (autres probabilités) if (randomValue < 9995) return 32; // 0.05%
if (randomValue < 9995) return 32; if (randomValue < 9998) return 64; // 0.03%
if (randomValue < 9998) return 64; if (randomValue < 9999) return 128;// 0.01%
if (randomValue < 9999) return 128; return 256; // 0.01%
return 256;
} }
/** /**
* Tente de déplacer et fusionner les tuiles vers le HAUT. * Tente de déplacer et fusionner les tuiles vers le HAUT.
* Met à jour le score interne en cas de fusion. * Met à jour le score interne et vérifie les états win/gameOver.
* Vérifie les conditions de victoire/défaite après le mouvement. * @return true si le plateau a été modifié, false sinon.
* @return true si au moins une tuile a bougé ou fusionné, false sinon.
*/ */
public boolean pushUp() { public boolean pushUp() { return processMove(MoveDirection.UP); }
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;
}
/** /**
* Tente de déplacer et fusionner les tuiles vers le BAS. * 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() { public boolean pushDown() { return processMove(MoveDirection.DOWN); }
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;
}
/** /**
* Tente de déplacer et fusionner les tuiles vers la GAUCHE. * 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() { public boolean pushLeft() { return processMove(MoveDirection.LEFT); }
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;
}
/** /**
* Tente de déplacer et fusionner les tuiles vers la DROITE. * 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() { public boolean pushRight() { return processMove(MoveDirection.RIGHT); }
boolean boardChanged = false; boolean[] hasMerged = new boolean[BOARD_SIZE];
for (int row = 0; row < BOARD_SIZE; row++) { /** Énumération interne pour clarifier le traitement des mouvements. */
hasMerged = new boolean[BOARD_SIZE]; private enum MoveDirection { UP, DOWN, LEFT, RIGHT }
for (int col = BOARD_SIZE - 2; col >= 0; col--) {
/**
* 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) { if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col); int currentCol = col; int currentValue = getCellValue(row, col);
while (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == 0) { setCellValue(row, currentCol + 1, currentValue); setCellValue(row, currentCol, 0); currentCol++; boardChanged = true; } int currentRow = row;
if (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == currentValue && !hasMerged[currentCol + 1]) { int currentCol = col;
int newValue = getCellValue(row, currentCol + 1) * 2; setCellValue(row, currentCol + 1, newValue); setCellValue(row, currentCol, 0);
currentScore += newValue; hasMerged[currentCol + 1] = true; boardChanged = true; // 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" * Format: "val,val,...,val,score"
* @return Chaîne sérialisée. * @return Chaîne représentant l'état du jeu.
*/ */
@NonNull @NonNull
@Override @Override
@ -271,53 +310,57 @@ public class Game {
} }
/** /**
* Crée un nouvel objet Game à partir d'une chaîne sérialisée (plateau + score). * Crée un objet Game à partir de sa représentation sérialisée (plateau + score).
* @param serializedState Chaîne générée par toString(). * Le meilleur score doit être défini séparément après la création.
* @return Nouvel objet Game, ou null si la désérialisation échoue. * @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) { public static Game deserialize(String serializedState) {
if (serializedState == null || serializedState.isEmpty()) return null; if (serializedState == null || serializedState.isEmpty()) return null;
String[] values = serializedState.split(","); 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; int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE]; int index = 0;
try { try {
for (int row = 0; row < BOARD_SIZE; row++) { for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); } 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); return new Game(newBoard, score);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; } } 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() { private void checkWinCondition() {
if (!gameWon) { // Optimisation: inutile de revérifier si déjà gagné 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; } 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). * Vérifie si la condition de fin de partie est atteinte (plateau plein ET aucun mouvement possible).
* Met à jour l'état `gameOver`. * Met à jour l'état interne `gameOver`.
*/ */
private void checkGameOverCondition() { private void checkGameOverCondition() {
if (hasEmptyCell()) { setGameOver(false); return; } // Si case vide, pas game over if (hasEmptyCell()) { setGameOver(false); return; } // Pas game over si case vide
// Vérifie fusions adjacentes possibles // 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++) { for (int r=0; r<BOARD_SIZE; r++) for (int c=0; c<BOARD_SIZE; c++) {
int current = getCellValue(r,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) || 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)) { (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 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() { 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;
@ -325,8 +368,8 @@ public class Game {
} }
/** /**
* Trouve et retourne la valeur de la tuile la plus élevée actuellement sur le plateau. * Retourne la valeur de la plus haute tuile présente sur le plateau.
* @return La valeur maximale trouvée. * @return La valeur maximale trouvée, ou 0 si le plateau est vide.
*/ */
public int getHighestTileValue() { public int getHighestTileValue() {
int maxTile = 0; int maxTile = 0;

View File

@ -1,30 +1,22 @@
// Fichier GameStats.java // Fichier GameStats.java
// Gère le stockage, le chargement et la mise à jour des statistiques du jeu 2048. /**
/* * Gère la collecte, la persistance (via SharedPreferences) et l'accès
Fonctions principales : * aux statistiques du jeu 2048, pour les modes solo et multijoueur (si applicable).
- 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.
*/ */
package legion.muyue.best2048; package legion.muyue.best2048;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class GameStats { public class GameStats {
// Clés SharedPreferences (inchangées) // --- Constantes pour SharedPreferences ---
private static final String PREFS_NAME = "Best2048_Prefs"; 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"; 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_GAMES_STARTED = "totalGamesStarted";
private static final String STATS_TOTAL_MOVES = "totalMoves"; private static final String STATS_TOTAL_MOVES = "totalMoves";
private static final String STATS_TOTAL_PLAY_TIME_MS = "totalPlayTimeMs"; 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_PERFECT_GAMES = "perfectGames";
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs"; private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs"; 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_WON = "multiplayerGamesWon";
private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed"; private static final String STATS_MP_GAMES_PLAYED = "multiplayerGamesPlayed";
private static final String STATS_MP_BEST_WINNING_STREAK = "multiplayerBestWinningStreak"; 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_LOSSES = "totalMultiplayerLosses";
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore"; 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 totalGamesPlayed;
private int totalGamesStarted; private int totalGamesStarted;
private int totalMoves; private int totalMoves;
private int currentMoves;
private long totalPlayTimeMs; private long totalPlayTimeMs;
private long currentGameStartTimeMs;
private int mergesThisGame;
private int totalMerges; private int totalMerges;
private int highestTile; private int highestTile;
private int numberOfTimesObjectiveReached; private int numberOfTimesObjectiveReached; // Nombre de victoires (>= 2048)
private int perfectGames; private int perfectGames; // Concept non défini ici
private long bestWinningTimeMs; private long bestWinningTimeMs;
private long worstWinningTimeMs; 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 multiplayerGamesWon;
private int multiplayerGamesPlayed; private int multiplayerGamesPlayed;
private int multiplayerBestWinningStreak; private int multiplayerBestWinningStreak;
@ -63,36 +63,39 @@ public class GameStats {
private long multiplayerTotalTimeMs; private long multiplayerTotalTimeMs;
private int totalMultiplayerLosses; private int totalMultiplayerLosses;
private int multiplayerHighestScore; private int multiplayerHighestScore;
private int overallHighScore;
/** Contexte nécessaire pour accéder aux SharedPreferences. */
private final Context context; private final Context context;
/** /**
* Constructeur de GameStats. * Constructeur. Initialise l'objet et charge immédiatement les statistiques
* Charge immédiatement les statistiques sauvegardées via SharedPreferences. * depuis les SharedPreferences.
* @param context Le contexte de l'application (nécessaire pour SharedPreferences). * @param context Contexte de l'application.
*/ */
public GameStats(Context context) { public GameStats(Context context) {
this.context = context; this.context = context.getApplicationContext(); // Utilise le contexte applicatif
loadStats(); loadStats();
} }
// --- Persistance (SharedPreferences) ---
/** /**
* Charge toutes les statistiques (générales et multijoueur) et le high score global * Charge toutes les statistiques persistantes depuis les SharedPreferences.
* depuis les SharedPreferences. * Appelé 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);
overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0); overallHighScore = prefs.getInt(HIGH_SCORE_KEY, 0);
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);
// ... (chargement de toutes les autres clés persistantes) ...
totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0); totalMoves = prefs.getInt(STATS_TOTAL_MOVES, 0);
totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0); totalPlayTimeMs = prefs.getLong(STATS_TOTAL_PLAY_TIME_MS, 0);
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); 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); worstWinningTimeMs = prefs.getLong(STATS_WORST_WINNING_TIME_MS, 0);
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);
@ -104,15 +107,16 @@ public class GameStats {
} }
/** /**
* Sauvegarde toutes les statistiques (générales et multijoueur) et le high score global * Sauvegarde toutes les statistiques persistantes dans les SharedPreferences.
* dans les SharedPreferences. * Appelé typiquement dans `onPause` de l'activité.
*/ */
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();
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_PLAYED, totalGamesPlayed);
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted); editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
// ... (sauvegarde de toutes les autres clés persistantes) ...
editor.putInt(STATS_TOTAL_MOVES, totalMoves); editor.putInt(STATS_TOTAL_MOVES, totalMoves);
editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs); editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs);
editor.putInt(STATS_TOTAL_MERGES, totalMerges); editor.putInt(STATS_TOTAL_MERGES, totalMerges);
@ -128,12 +132,15 @@ 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();
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. * Doit être appelée au début de chaque nouvelle partie.
* Incrémente le nombre de parties démarrées et réinitialise les compteurs de la partie en cours. * Incrémente le compteur de parties démarrées et réinitialise les stats de la partie en cours.
*/ */
public void startGame() { public void startGame() {
totalGamesStarted++; totalGamesStarted++;
@ -143,8 +150,7 @@ public class GameStats {
} }
/** /**
* Enregistre un mouvement effectué pendant la partie en cours. * Enregistre un mouvement réussi (qui a modifié le plateau).
* Incrémente le compteur de mouvements de la partie et le compteur total.
*/ */
public void recordMove() { public void recordMove() {
currentMoves++; currentMoves++;
@ -152,8 +158,8 @@ public class GameStats {
} }
/** /**
* Enregistre une ou plusieurs fusions survenues pendant la partie en cours. * Enregistre une ou plusieurs fusions survenues lors d'un mouvement.
* @param numberOfMerges Le nombre de fusions à ajouter (idéalement précis). * @param numberOfMerges Le nombre de fusions (si connu, sinon 1 par défaut).
*/ */
public void recordMerge(int numberOfMerges) { public void recordMerge(int numberOfMerges) {
if (numberOfMerges > 0) { 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. * Met à jour la statistique de la plus haute tuile atteinte globalement.
* @param tileValue La valeur de la tuile candidate. * @param tileValue La valeur de la tuile la plus haute de la partie en cours.
*/ */
public void updateHighestTile(int tileValue) { public void updateHighestTile(int tileValue) {
if (tileValue > this.highestTile) { 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). * Enregistre une victoire et met à jour les temps associés.
* @param timeTakenMs Le temps mis pour gagner cette partie en millisecondes. * @param timeTakenMs Temps écoulé pour cette partie gagnante.
*/ */
public void recordWin(long timeTakenMs) { public void recordWin(long timeTakenMs) {
numberOfTimesObjectiveReached++; numberOfTimesObjectiveReached++;
endGame(timeTakenMs); // Finalise les stats de la partie
if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; } if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; }
if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = 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() { public void recordLoss() {
// Calcule le temps écoulé avant de finaliser
endGame(System.currentTimeMillis() - currentGameStartTimeMs); endGame(System.currentTimeMillis() - currentGameStartTimeMs);
} }
/** /**
* Finalise les statistiques à la fin d'une partie (incrémente parties jouées, ajoute temps de jeu). * Finalise les statistiques générales à la fin d'une partie (victoire ou défaite).
* @param timeTakenMs Le temps total de la partie qui vient de se terminer. * @param timeTakenMs Temps total de la partie terminée.
*/ */
public void endGame(long timeTakenMs) { public void endGame(long timeTakenMs) {
totalGamesPlayed++; totalGamesPlayed++;
@ -201,8 +207,9 @@ public class GameStats {
} }
/** /**
* Ajoute une durée au temps de jeu total cumulé. * Ajoute une durée (en ms) au temps de jeu total enregistré.
* @param durationMs Durée à ajouter en millisecondes. * Typiquement appelé dans `onPause`.
* @param durationMs Durée à ajouter.
*/ */
public void addPlayTime(long durationMs) { public void addPlayTime(long durationMs) {
if (durationMs > 0) { if (durationMs > 0) {
@ -210,13 +217,13 @@ public class GameStats {
} }
} }
// --- Getters --- // --- Getters pour l'affichage ---
public int getTotalGamesPlayed() { return totalGamesPlayed; } public int getTotalGamesPlayed() { return totalGamesPlayed; }
public int getTotalGamesStarted() { return totalGamesStarted; } public int getTotalGamesStarted() { return totalGamesStarted; }
public int getTotalMoves() { return totalMoves; } public int getTotalMoves() { return totalMoves; }
public int getCurrentMoves() { return currentMoves; } public int getCurrentMoves() { return currentMoves; }
public long getTotalPlayTimeMs() { return totalPlayTimeMs; } 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 getMergesThisGame() { return mergesThisGame; }
public int getTotalMerges() { return totalMerges; } public int getTotalMerges() { return totalMerges; }
public int getHighestTile() { return highestTile; } public int getHighestTile() { return highestTile; }
@ -224,6 +231,8 @@ public class GameStats {
public int getPerfectGames() { return perfectGames; } public int getPerfectGames() { return perfectGames; }
public long getBestWinningTimeMs() { return bestWinningTimeMs; } public long getBestWinningTimeMs() { return bestWinningTimeMs; }
public long getWorstWinningTimeMs() { return worstWinningTimeMs; } public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
public int getOverallHighScore() { return overallHighScore; } // Getter pour HS global
// Getters Multiplayer
public int getMultiplayerGamesWon() { return multiplayerGamesWon; } public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; } public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; } public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
@ -231,32 +240,25 @@ public class GameStats {
public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; } public long getMultiplayerTotalTimeMs() { return multiplayerTotalTimeMs; }
public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; } public int getTotalMultiplayerLosses() { return totalMultiplayerLosses; }
public int getMultiplayerHighestScore() { return multiplayerHighestScore; } public int getMultiplayerHighestScore() { return multiplayerHighestScore; }
public int getOverallHighScore() { return overallHighScore; } // Getter pour le HS global
// --- Setters --- // --- Setters ---
/** /** Met à jour la valeur interne du high score global. */
* 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.
*/
public void setHighestScore(int highScore) { public void setHighestScore(int highScore) {
// On met à jour seulement si la valeur externe est supérieure, // Met à jour si la nouvelle valeur est meilleure
// car GameStats gère aussi la mise à jour via les scores des parties. if (highScore > this.overallHighScore) {
// Ou plus simplement, on fait confiance à la valeur lue par MainActivity.
this.overallHighScore = highScore; this.overallHighScore = highScore;
// La sauvegarde se fait via saveStats() globalement
} }
/**
* 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;
} }
/** Définit le timestamp de début de la partie en cours. */
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; } 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; } 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; } public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; }
/** /**
@ -264,6 +266,7 @@ public class GameStats {
* @param milliseconds Durée en millisecondes. * @param milliseconds Durée en millisecondes.
* @return Chaîne de temps formatée. * @return Chaîne de temps formatée.
*/ */
@SuppressLint("DefaultLocale")
public static String formatTime(long milliseconds) { public static String formatTime(long milliseconds) {
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds); long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60; long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60;

View File

@ -1,32 +1,14 @@
// Fichier MainActivity.java // Fichier MainActivity.java
// Activité principale de l'application 2048, gère l'interface utilisateur et la coordination du jeu. /**
/* * Activité principale de l'application 2048.
Fonctions principales : * Gère l'interface utilisateur (plateau, scores, boutons), coordonne les interactions
- findViews() : Récupère les références des éléments UI du layout XML. * avec la logique du jeu (classe Game) et la gestion des statistiques (classe GameStats).
- initializeGameAndStats() : Crée les instances de Game et GameStats, charge les données sauvegardées. * Gère également le cycle de vie de l'application et la persistance de l'état du jeu.
- 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.
*/ */
package legion.muyue.best2048; package legion.muyue.best2048;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue; import android.util.TypedValue;
@ -46,7 +28,7 @@ import android.widget.Button;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
// --- UI Elements ---
private GridLayout boardGridLayout; private GridLayout boardGridLayout;
private TextView currentScoreTextView; private TextView currentScoreTextView;
private TextView highestScoreTextView; private TextView highestScoreTextView;
@ -55,25 +37,25 @@ public class MainActivity extends AppCompatActivity {
private Button statisticsButton; private Button statisticsButton;
private Button menuButton; private Button menuButton;
private ViewStub statisticsViewStub; 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 Game game;
private GameStats gameStats; // Instance pour gérer les stats private GameStats gameStats;
private static final int BOARD_SIZE = 4; 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 SharedPreferences preferences;
private static final String PREFS_NAME = "Best2048_Prefs"; 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";
private static final String GAME_STATE_KEY = "game_state"; private static final String GAME_STATE_KEY = "game_state";
// --- Activity Lifecycle ---
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -81,26 +63,25 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
findViews(); // Récupère les vues findViews();
initializeGameAndStats(); // Initialise Game, GameStats et charge les données initializeGameAndStats();
setupListeners(); // Attache les listeners setupListeners();
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.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()) { 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 (statisticsVisible) {
if (inflatedStatsView != null) { // Si déjà gonflé if (inflatedStatsView != null) {
updateStatisticsTextViews(); // Met à jour les données affichées updateStatisticsTextViews();
inflatedStatsView.setVisibility(View.VISIBLE); inflatedStatsView.setVisibility(View.VISIBLE);
multiplayerButton.setVisibility(View.GONE); multiplayerButton.setVisibility(View.GONE);
} else { } else {
// Si pas encore gonflé (cas rare mais possible), on le fait afficher
toggleStatistics(); toggleStatistics();
} }
} }
@ -109,18 +90,18 @@ public class MainActivity extends AppCompatActivity {
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
// Sauvegarde l'état et les stats si le jeu existe
if (game != null && gameStats != null) { if (game != null && gameStats != null) {
// Met à jour le temps total SI la partie n'est pas terminée
if (!game.isGameOver() && !game.isGameWon()) { 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 saveGame();
gameStats.saveStats(); // Sauvegarde toutes les stats via GameStats gameStats.saveStats();
} }
} }
// --- Initialisation ---
/** /**
* Récupère les références des vues du layout principal via leur ID. * 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() { private void initializeGameAndStats() {
preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
gameStats = new GameStats(this); gameStats = new GameStats(this);
loadGame(); // Charge jeu et met à jour high score loadGame();
updateUI(); updateUI();
if (game == null) { if (game == null) {
startNewGame(); // Assure une partie valide si chargement échoue startNewGame();
} else { } else {
// Détermine l'état initial basé sur le jeu chargé
if (game.isGameOver()) { if (game.isGameOver()) {
currentGameState = GameFlowState.GAME_OVER; currentGameState = GameFlowState.GAME_OVER;
} else if (game.isGameWon()) { } 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; currentGameState = GameFlowState.WON_DIALOG_SHOWN;
} else { } else {
currentGameState = GameFlowState.PLAYING; currentGameState = GameFlowState.PLAYING;
// Redémarre le timer dans onResume
} }
} }
} }
@ -172,20 +153,20 @@ public class MainActivity extends AppCompatActivity {
}); });
statisticsButton.setOnClickListener(v -> { statisticsButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
toggleStatistics(); // Affiche/masque les stats toggleStatistics();
}); });
menuButton.setOnClickListener(v -> { menuButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press));
showMenu(); // Affiche dialogue placeholder showMenu();
}); });
multiplayerButton.setOnClickListener(v -> { multiplayerButton.setOnClickListener(v -> {
v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); 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). * 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++) { for (int col = 0; col < BOARD_SIZE; col++) {
TextView tileTextView = new TextView(this); TextView tileTextView = new TextView(this);
int value = game.getCellValue(row, col); int value = game.getCellValue(row, col);
setTileStyle(tileTextView, value); // Applique le style setTileStyle(tileTextView, value);
// Définit les LayoutParams pour que la tuile remplisse la cellule du GridLayout
GridLayout.LayoutParams params = new GridLayout.LayoutParams(); GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.width = 0; params.height = 0; // Poids gère la taille params.width = 0; params.height = 0;
params.rowSpec = GridLayout.spec(row, 1f); // Prend 1 fraction de l'espace en hauteur params.rowSpec = GridLayout.spec(row, 1f);
params.columnSpec = GridLayout.spec(col, 1f); // Prend 1 fraction de l'espace en largeur params.columnSpec = GridLayout.spec(col, 1f);
int margin = (int) getResources().getDimension(R.dimen.tile_margin); 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); 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). * @param value La valeur numérique de la tuile (0 pour vide).
*/ */
private void setTileStyle(TextView tileTextView, int value) { 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.setText(value > 0 ? String.valueOf(value) : "");
tileTextView.setGravity(Gravity.CENTER); tileTextView.setGravity(Gravity.CENTER);
tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); 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. * 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). * @param direction La direction du swipe détecté (UP, DOWN, LEFT, RIGHT).
*/ */
private void handleSwipe(Direction direction) { 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) { if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) {
return; return;
} }
// Stocker le score avant le mouvement pour calculer le delta
int scoreBefore = game.getCurrentScore(); int scoreBefore = game.getCurrentScore();
// Indique si le mouvement a effectivement changé l'état du plateau
boolean boardChanged = false; 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) { switch (direction) {
case UP: case UP:
boardChanged = game.pushUp(); boardChanged = game.pushUp();
@ -318,63 +298,63 @@ public class MainActivity extends AppCompatActivity {
break; break;
} }
// --- 2. Traiter les conséquences SI le plateau a changé ---
if (boardChanged) { if (boardChanged) {
// Mettre à jour les statistiques liées au mouvement réussi
gameStats.recordMove(); gameStats.recordMove();
int scoreAfter = game.getCurrentScore(); int scoreAfter = game.getCurrentScore();
int scoreDelta = scoreAfter - scoreBefore; int scoreDelta = scoreAfter - scoreBefore;
if (scoreDelta > 0) { if (scoreDelta > 0) {
// Supposition simpliste : une augmentation de score implique au moins une fusion
gameStats.recordMerge(1); gameStats.recordMerge(1);
// Vérifier et mettre à jour le meilleur score si nécessaire
if (scoreAfter > game.getHighestScore()) { if (scoreAfter > game.getHighestScore()) {
game.setHighestScore(scoreAfter); // Met à jour dans l'objet Game game.setHighestScore(scoreAfter);
gameStats.setHighestScore(scoreAfter); // Met à jour et sauvegarde dans GameStats gameStats.setHighestScore(scoreAfter);
} }
} }
// Mettre à jour la tuile la plus haute atteinte
gameStats.updateHighestTile(game.getHighestTileValue()); gameStats.updateHighestTile(game.getHighestTileValue());
// Ajouter une nouvelle tuile aléatoire sur le plateau
game.addNewTile(); game.addNewTile();
// Mettre à jour l'affichage complet du plateau et des scores
updateUI(); 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) { 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) { if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) {
currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Mettre à jour l'état de flux currentGameState = GameFlowState.WON_DIALOG_SHOWN;
// Enregistrer les statistiques de victoire
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordWin(timeTaken); gameStats.recordWin(timeTaken);
// Afficher la boîte de dialogue de victoire
showGameWonKeepPlayingDialog(); 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()) { } else if (game.isGameOver()) {
currentGameState = GameFlowState.GAME_OVER; // Mettre à jour l'état de flux currentGameState = GameFlowState.GAME_OVER;
// Enregistrer les statistiques de défaite et finaliser la partie
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordLoss(); gameStats.recordLoss();
gameStats.endGame(timeTaken); // Finalise le temps, etc. gameStats.endGame(timeTaken);
// Afficher la boîte de dialogue de Game Over
showGameOverDialog(); showGameOverDialog();
if (!boardChanged) { 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. * crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI.
*/ */
private void startNewGame() { private void startNewGame() {
gameStats.startGame(); // Réinitialise stats de partie gameStats.startGame();
game = new Game(); // Crée un nouveau jeu game = new Game();
game.setHighestScore(gameStats.getOverallHighScore()); // Applique HS global game.setHighestScore(gameStats.getOverallHighScore());
currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER currentGameState = GameFlowState.PLAYING;
updateUI(); // Met à jour affichage updateUI();
} }
/** /**
@ -410,24 +390,24 @@ public class MainActivity extends AppCompatActivity {
private void showGameWonKeepPlayingDialog() { private void showGameWonKeepPlayingDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater(); 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.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 keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton);
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon); Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon);
final AlertDialog dialog = builder.create(); final AlertDialog dialog = builder.create();
keepPlayingButton.setOnClickListener(v -> { keepPlayingButton.setOnClickListener(v -> {
// L'état est déjà WON_DIALOG_SHOWN, on ne fait rien de spécial, le jeu continue.
dialog.dismiss(); dialog.dismiss();
}); });
newGameButton.setOnClickListener(v -> { newGameButton.setOnClickListener(v -> {
dialog.dismiss(); dialog.dismiss();
startNewGame(); // Démarre une nouvelle partie startNewGame();
}); });
dialog.show(); dialog.show();
@ -440,35 +420,35 @@ public class MainActivity extends AppCompatActivity {
private void showGameOverDialog() { private void showGameOverDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater(); 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.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); TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver);
Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver); Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver);
Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver); 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())); messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore()));
final AlertDialog dialog = builder.create(); final AlertDialog dialog = builder.create();
newGameButton.setOnClickListener(v -> { newGameButton.setOnClickListener(v -> {
dialog.dismiss(); dialog.dismiss();
startNewGame(); // Démarre une nouvelle partie startNewGame();
}); });
quitButton.setOnClickListener(v -> { quitButton.setOnClickListener(v -> {
dialog.dismiss(); dialog.dismiss();
finish(); // Ferme l'application finish();
}); });
dialog.show(); dialog.show();
} }
// --- Gestion des Statistiques (UI) ---
/** /**
* Affiche ou masque le panneau de statistiques. * Affiche ou masque le panneau de statistiques.
@ -477,20 +457,20 @@ public class MainActivity extends AppCompatActivity {
private void toggleStatistics() { private void toggleStatistics() {
statisticsVisible = !statisticsVisible; statisticsVisible = !statisticsVisible;
if (statisticsVisible) { if (statisticsVisible) {
if (inflatedStatsView == null) { // Gonfle si pas encore fait if (inflatedStatsView == null) {
inflatedStatsView = statisticsViewStub.inflate(); inflatedStatsView = statisticsViewStub.inflate();
// Attache listener au bouton Back une fois la vue gonflée
Button backButton = inflatedStatsView.findViewById(R.id.backButton); 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 updateStatisticsTextViews();
inflatedStatsView.setVisibility(View.VISIBLE); // Affiche inflatedStatsView.setVisibility(View.VISIBLE);
multiplayerButton.setVisibility(View.GONE); // Masque bouton multi multiplayerButton.setVisibility(View.GONE);
} else { } else {
if (inflatedStatsView != null) { // Masque si la vue existe if (inflatedStatsView != null) {
inflatedStatsView.setVisibility(View.GONE); 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() { private void updateStatisticsTextViews() {
if (inflatedStatsView == null || gameStats == null) return; 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 highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label);
TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label); TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label);
TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_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 multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label);
TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label); TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label);
TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_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 totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label);
TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label); TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label);
TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game); 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())); highScoreStatsLabel.setText(getString(R.string.high_score_stats, gameStats.getOverallHighScore()));
totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed())); totalGamesPlayedLabel.setText(getString(R.string.total_games_played, gameStats.getTotalGamesPlayed()));
totalGamesStartedLabel.setText(getString(R.string.total_games_started, gameStats.getTotalGamesStarted())); 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())); totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, gameStats.getTotalMultiplayerLosses()));
multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, gameStats.getMultiplayerHighestScore())); 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"; String winPercentage = (gameStats.getTotalGamesStarted() > 0) ? String.format("%.2f%%", ((double) gameStats.getNumberOfTimesObjectiveReached() / gameStats.getTotalGamesStarted()) * 100) : "N/A";
winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage)); winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage));
String multiplayerWinRate = (gameStats.getMultiplayerGamesPlayed() > 0) ? String.format("%.2f%%", ((double) gameStats.getMultiplayerGamesWon() / gameStats.getMultiplayerGamesPlayed()) * 100) : "N/A"; 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)); multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate));
// Calculs Temps
totalPlayTimeLabel.setText(getString(R.string.total_play_time, GameStats.formatTime(gameStats.getTotalPlayTimeMs()))); 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; 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))); 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")); 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. */ /** Affiche un dialogue placeholder pour le menu. */
private void showMenu() { /* ... (inchangé) ... */ private void showMenu() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Menu").setMessage("Fonctionnalité de menu à venir !").setPositiveButton("OK", null); builder.setTitle("Menu").setMessage("Fonctionnalité de menu à venir !").setPositiveButton("OK", null);
builder.create().show(); builder.create().show();
} }
/** Affiche un dialogue placeholder pour le multijoueur. */ /** Affiche un dialogue placeholder pour le multijoueur. */
private void showMultiplayerScreen() { /* ... (inchangé) ... */ private void showMultiplayerScreen() {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null); builder.setTitle("Multijoueur").setMessage("Fonctionnalité multijoueur à venir !").setPositiveButton("OK", null);
builder.create().show(); builder.create().show();
} }
// --- Sauvegarde / Chargement ---
/** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */ /** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */
private void saveGame() { private void saveGame() {
SharedPreferences.Editor editor = preferences.edit(); SharedPreferences.Editor editor = preferences.edit();
if (game != null) { if (game != null) {
editor.putString(GAME_STATE_KEY, game.toString()); // Sérialise Game (plateau + score courant) editor.putString(GAME_STATE_KEY, game.toString());
editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); // Sauvegarde le HS contenu dans Game editor.putInt(HIGH_SCORE_KEY, game.getHighestScore());
} else { } else {
editor.remove(GAME_STATE_KEY); // Optionnel: nettoyer si pas de jeu editor.remove(GAME_STATE_KEY);
} }
editor.apply(); editor.apply();
} }
@ -594,28 +574,27 @@ public class MainActivity extends AppCompatActivity {
/** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */ /** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */
private void loadGame() { private void loadGame() {
String gameStateString = preferences.getString(GAME_STATE_KEY, null); 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); gameStats.setHighestScore(savedHighScore);
} }
if (gameStateString != null) { if (gameStateString != null) {
game = Game.deserialize(gameStateString); game = Game.deserialize(gameStateString);
if (game != null) { if (game != null) {
game.setHighestScore(savedHighScore); // Applique HS à Game game.setHighestScore(savedHighScore);
// 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
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 = new Game();
game.setHighestScore(savedHighScore); game.setHighestScore(savedHighScore);
currentGameState = GameFlowState.PLAYING; 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 // Fichier OnSwipeTouchListener.java
// Classe utilitaire pour détecter les gestes de balayage (swipe). /**
/* * Listener de vue personnalisé qui détecte les gestes de balayage (swipe)
Fonctions principales : * dans les quatre directions cardinales et notifie un listener externe.
- Utilise GestureDetector pour détecter les swipes. * Utilise {@link GestureDetector} pour l'analyse des gestes.
- 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.
*/ */
package legion.muyue.best2048; package legion.muyue.best2048;
import android.annotation.SuppressLint; // Ajout import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
// import android.location.GnssAntennaInfo; // Import inutile trouvé dans le dump
import android.view.GestureDetector; 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; // Ajout
public class OnSwipeTouchListener implements View.OnTouchListener { public class OnSwipeTouchListener implements View.OnTouchListener {
/** Détecteur de gestes standard d'Android. */
private final GestureDetector gestureDetector; private final GestureDetector gestureDetector;
/** Listener externe à notifier lors de la détection d'un swipe. */
private final SwipeListener listener; private final SwipeListener listener;
/** /**
* Interface pour les événements de swipe. Toute classe qui veut réagir aux swipes * Interface à implémenter par les classes souhaitant réagir aux événements de swipe.
* doit implémenter cette interface et passer une instance à OnSwipeTouchListener.
*/ */
public interface SwipeListener { 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(); 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(); 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(); 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(); void onSwipeRight();
} }
/** /**
* Constructeur de la classe OnSwipeTouchListener. * Constructeur.
* * @param context Contexte applicatif, nécessaire pour `GestureDetector`.
* @param ctx Le contexte de l'application. Nécessaire pour GestureDetector. * @param listener Instance qui recevra les notifications de swipe. Ne doit pas être null.
* @param listener L'instance qui écoutera les événements de swipe.
*/ */
public OnSwipeTouchListener(Context ctx, SwipeListener listener) { public OnSwipeTouchListener(Context context, @NonNull SwipeListener listener) {
gestureDetector = new GestureDetector(ctx, new GestureListener()); this.gestureDetector = new GestureDetector(context, new GestureListener());
this.listener = listener; this.listener = listener;
} }
/** /**
* Méthode appelée lorsqu'un événement tactile se produit sur la vue attachée. * Intercepte les événements tactiles sur la vue associée et les délègue
* Elle transmet l'événement au GestureDetector pour analyse. * au {@link GestureDetector} pour analyse.
* * @param v La vue touchée.
* @param v La vue sur laquelle l'événement tactile s'est produit. * @param event L'événement tactile.
* @param event L'objet MotionEvent décrivant l'événement tactile. * @return true si le geste a été consommé par le détecteur, false sinon.
* @return true si l'événement a été géré, false sinon.
*/ */
@SuppressLint("ClickableViewAccessibility") // Ajout @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),
// il retournera true, et l'événement ne sera pas propagé davantage.
return gestureDetector.onTouchEvent(event); return gestureDetector.onTouchEvent(event);
} }
/** /**
* Classe interne qui étend GestureDetector.SimpleOnGestureListener pour gérer * Classe interne implémentant l'écouteur de gestes pour détecter le 'fling' (balayage rapide).
* spécifiquement les gestes de balayage (fling).
*/ */
private final class GestureListener extends GestureDetector.SimpleOnGestureListener { private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
private static final int SWIPE_THRESHOLD = 100; // Distance minimale du swipe (en pixels). /** Distance minimale (en pixels) pour qu'un mouvement soit considéré comme un swipe. */
private static final int SWIPE_VELOCITY_THRESHOLD = 100; // Vitesse minimale du swipe (en pixels par seconde). 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é. * Toujours retourner true pour onDown garantit que les événements suivants
* On retourne toujours 'true' pour indiquer qu'on gère cet événement. * (comme onFling) seront bien reçus par ce listener.
*
* @param e L'objet MotionEvent décrivant l'appui initial.
* @return true, car on gère toujours l'événement 'onDown'.
*/ */
@Override @Override
public boolean onDown(@NonNull MotionEvent e) { // Ajout @NonNull public boolean onDown(@NonNull MotionEvent e) {
return true; return true;
} }
/** /**
* Méthode appelée lorsqu'un geste de balayage (fling) est détecté. * 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
* @param e1 L'objet MotionEvent du premier appui (début du swipe). Peut être null. * et notifie le {@link SwipeListener} externe.
* @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.
*/ */
@Override @Override
public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { // Ajout @NonNull public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
// 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; // Point de départ est nécessaire
if (e1 == null) {
return false;
}
boolean result = false; boolean result = false;
try { try {
float diffY = e2.getY() - e1.getY(); // Différence de position sur l'axe Y. float diffY = e2.getY() - e1.getY();
float diffX = e2.getX() - e1.getX(); // Différence de position sur l'axe X. 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)) { 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 (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) { if (diffX > 0) {
listener.onSwipeRight(); // Swipe vers la droite. listener.onSwipeRight();
} else { } else {
listener.onSwipeLeft(); // Swipe vers la gauche. listener.onSwipeLeft();
} }
result = true; result = true; // Geste horizontal traité
} }
} else { } else {
// Swipe vertical. // Mouvement principalement vertical
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) {
listener.onSwipeBottom(); // Swipe vers le bas. listener.onSwipeBottom();
} else { } else {
listener.onSwipeTop(); // Swipe vers le haut. listener.onSwipeTop();
} }
result = true; result = true; // Geste vertical traité
} }
} }
} catch (Exception exception) { } 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; return result;
} }
} }
} } // Fin OnSwipeTouchListener