Refactor: Finalisation et externalisation des statistiques

- Création de la classe GameStats pour encapsuler les données et la logique des statistiques.
- Implémentation de loadStats() et saveStats() dans GameStats utilisant SharedPreferences.
- Ajout de méthodes dans GameStats pour enregistrer les événements du jeu (startGame, recordMove, recordMerge, recordWin, recordLoss, endGame, updateHighestTile, addPlayTime, setCurrentGameStartTimeMs, setHighestScore).
- Ajout de getters dans GameStats pour l'affichage et de méthodes pour les valeurs calculées (moyennes, pourcentages).
- Déplacement de formatTime() dans GameStats.
- Refactorisation de MainActivity:
  - Suppression des champs et méthodes de statistiques individuelles.
  - Utilisation d'une instance de GameStats pour gérer les statistiques.
  - Mise à jour de handleSwipe, startNewGame, onPause, onResume pour appeler GameStats.
  - Mise à jour de updateStatisticsTextViews pour utiliser les getters de GameStats.
  - MainActivity gère maintenant le chargement/sauvegarde de l'état sérialisé du jeu et du high score via SharedPreferences, en passant le high score à Game via setHighestScore.
- Refactorisation de Game:
  - Suppression de la dépendance au Contexte et SharedPreferences.
  - Suppression de la gestion interne du high score (reçoit via setHighestScore).
  - Ajout de getBoard() et getHighestTileValue().
  - Modification du constructeur et de deserialize pour être indépendants du contexte et du high score.
- Ajout/Mise à jour des commentaires JavaDoc dans les classes modifiées.
This commit is contained in:
Augustin ROUX 2025-04-04 13:12:59 +02:00
parent 73ab81e208
commit 9d8d2c5c62
4 changed files with 803 additions and 1053 deletions

View File

@ -1,640 +1,336 @@
// Fichier Game.java
// Ce fichier contient la logique principale du jeu 2048. Il est indépendant de l'interface utilisateur.
// 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 :
- gameBoard : Matrice 2D (int[][]) représentant la grille du jeu.
- score, highScore : Suivi du score et du meilleur score.
- addNewNumbers() : Ajoute une nouvelle tuile (2, 4 ou 8) aléatoirement sur la grille.
- pushUp(), pushDown(), pushLeft(), pushRight() : Logique de déplacement et de fusion des tuiles.
- serialize(), deserialize() : Sauvegarde et restauration de l'état du jeu (grille, score, meilleur score) en chaînes de caractères.
- loadHighScore(), saveHighScore(), loadGameState(), saveGameState() : Utilisation de SharedPreferences pour persister les données.
- Constructeurs : Initialisation de la grille et chargement/restauration des données.
- 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 et appelle ses méthodes pour la logique du jeu. MainActivity affiche l'état du jeu.
- SharedPreferences : Game utilise SharedPreferences pour sauvegarder et charger le meilleur score et l'état du jeu.
- 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;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Game {
private int[][] board; // Renommé
private final Random randomNumberGenerator; // Renommé
private int currentScore = 0; // Renommé
private int highestScore = 0; // Renommé
private static final int BOARD_SIZE = 4; // Ajout constante
private static final String PREFS_NAME = "Best2048_Prefs"; // Ajout constante
private static final String HIGH_SCORE_KEY = "high_score"; // Ajout constante
private static final String GAME_STATE_KEY = "game_state"; // Ajout constante
private final Context context; // Ajout contexte
private boolean gameWon = false; // Ajout état
private boolean gameOver = false; // Ajout état
private int[][] board;
private final Random randomNumberGenerator;
private int currentScore = 0;
private int highestScore = 0; // Stocke le HS fourni par MainActivity
private static final int BOARD_SIZE = 4;
private boolean gameWon = false;
private boolean gameOver = false;
/**
* Constructeur principal de la classe Game.
*
* @param context Le contexte Android (généralement une Activity). Nécessaire pour accéder aux SharedPreferences.
* Constructeur pour une nouvelle partie.
* Initialise un plateau vide, définit le score à 0 et ajoute deux tuiles initiales.
*/
public Game(Context context) {
this.context = context;
public Game() {
this.randomNumberGenerator = new Random();
this.board = new int[BOARD_SIZE][BOARD_SIZE];
loadHighScore();
loadGameState(); // Charge l'état précédent ou initialise
// Le highScore sera défini par MainActivity après l'instanciation.
initializeNewBoard();
}
/**
* Constructeur pour la restauration d'une partie précédemment sauvegardée.
*
* @param board Le plateau de jeu restauré (l'état des tuiles).
* @param score Le score au moment de la sauvegarde.
* @param highScore Le meilleur score enregistré au moment de la sauvegarde.
* @param context Le contexte Android, nécessaire pour les SharedPreferences.
* Constructeur utilisé lors de la restauration d'une partie sauvegardée.
* @param board Le plateau de jeu restauré.
* @param score Le score courant restauré.
*/
public Game(int[][] board, int score, int highScore, Context context) {
public Game(int[][] board, int score) {
this.board = board;
this.currentScore = score;
this.highestScore = highScore;
this.context = context;
this.randomNumberGenerator = new Random();
// Le highScore sera défini par MainActivity après l'instanciation.
checkWinCondition(); // Recalcule l'état basé sur le plateau chargé
checkGameOverCondition();
}
// --- Getters / Setters ---
/**
* Récupère la valeur d'une cellule spécifique sur le plateau.
*
* @param row L'index de la ligne (0 à BOARD_SIZE - 1).
* @param column L'index de la colonne (0 à BOARD_SIZE - 1).
* @return La valeur de la cellule à la position spécifiée.
* @throws IllegalArgumentException Si row ou column sont en dehors des limites du plateau.
* 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é).
*/
public int getCellValue(int row, int column) {
if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) {
throw new IllegalArgumentException("Indices de ligne ou de colonne hors limites : row=" + row + ", column=" + column);
}
if (row < 0 || row >= BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { return 0; }
return this.board[row][column];
}
/**
* Définit la valeur d'une cellule spécifique sur le plateau.
*
* @param row L'index de la ligne (0 à BOARD_SIZE - 1).
* @param col L'index de la colonne (0 à BOARD_SIZE - 1).
* @param value La nouvelle valeur de la cellule.
* @throws IllegalArgumentException Si row ou col sont en dehors des limites du plateau.
* 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.
*/
public void setCellValue(int row, int col, int value) {
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
throw new IllegalArgumentException("Indices de ligne ou de colonne hors limites : row=" + row + ", col=" + col);
}
if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return; }
this.board[row][col] = value;
}
/**
* Récupère le score actuel de la partie.
*
* @return Le score actuel.
*/
public int getCurrentScore() {
return this.currentScore;
}
/** @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). */
public int getHighestScore() { return highestScore; }
/**
* Met à jour le score actuel.
*
* @param currentScore Le nouveau score actuel.
* 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.
*/
public void setCurrentScore(int currentScore) {
this.currentScore = currentScore;
}
public void setHighestScore(int highScore) { this.highestScore = highScore; }
/**
* Récupère le meilleur score enregistré.
*
* @return Le meilleur score.
*/
public int getHighestScore() {
return this.highestScore;
}
/** @return L'état de victoire de la partie (true si >= 2048 atteint). */
public boolean isGameWon() { return gameWon; }
/**
* Met à jour le meilleur score. Utilisé lors du chargement et après une partie.
*
* @param highestScore Le nouveau meilleur score.
*/
public void setHighestScore(int highestScore) {
this.highestScore = highestScore;
}
public void setGameWon(boolean gameWon) { this.gameWon = gameWon; }
/**
* Met à jour le board. Utilisé lors du chargement.
*
* @param board Le nouveau board.
*/
public void setBoard(int[][] board){
this.board = board;
}
/** @return L'état de fin de partie (true si aucun mouvement possible). */
public boolean isGameOver() { return gameOver; }
/**
* Récupère le générateur de nombres aléatoires.
*
* @return Le générateur de nombres aléatoires.
*/
public Random getRandomNumberGenerator() {
return this.randomNumberGenerator;
}
public void setGameOver(boolean gameOver) { this.gameOver = gameOver; }
/**
* Indique si le joueur a gagné la partie (atteint une tuile de 2048).
*
* @return true si le joueur a gagné, false sinon.
*/
public boolean isGameWon() {
return this.gameWon;
}
/**
* Indique si la partie est terminée (plus de mouvements possibles).
*
* @return true si la partie est terminée, false sinon.
*/
public boolean isGameOver() {
return this.gameOver;
}
/**
* Définit l'état de la partie gagnée.
*
* @param gameWon true si le joueur a gagné, false sinon.
*/
public void setGameWon(boolean gameWon) {
this.gameWon = gameWon;
}
/**
* Définit l'état de la partie terminée.
*
* @param gameOver true si la partie est terminée, false sinon.
*/
public void setGameOver(boolean gameOver) {
this.gameOver = gameOver;
}
/**
* Charge le meilleur score à partir des préférences partagées (SharedPreferences).
*/
public void loadHighScore() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
setHighestScore(prefs.getInt(HIGH_SCORE_KEY, 0));
}
/**
* Enregistre le meilleur score actuel dans les préférences partagées (SharedPreferences).
*/
public void saveHighScore() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(HIGH_SCORE_KEY, getHighestScore());
editor.apply();
}
/**
* Charge l'état de la partie (plateau, score) à partir des préférences partagées.
*/
private void loadGameState() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
String savedState = prefs.getString(GAME_STATE_KEY, null);
if (savedState != null) {
Game savedGame = deserialize(savedState, context);
if (savedGame != null) { // Vérifier si la désérialisation a réussi
setBoard(savedGame.board);
setCurrentScore(savedGame.currentScore);
// Le meilleur score est déjà chargé par loadHighScore() appelé dans le constructeur principal
} else {
// Si l'état sauvegardé est invalide, commence une nouvelle partie
initializeNewBoard();
}
} else {
// Si aucun état n'est sauvegardé, commence une nouvelle partie
initializeNewBoard();
/** @return Une copie du plateau de jeu actuel (pour la sauvegarde externe). */
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);
}
return copy;
}
/**
* Initialise le plateau pour une nouvelle partie (typiquement avec deux tuiles).
* Initialise ou réinitialise le plateau pour une nouvelle partie.
* Met le score à 0 et ajoute deux tuiles aléatoires.
*/
private void initializeNewBoard() {
this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Assure que le plateau est vide
this.board = new int[BOARD_SIZE][BOARD_SIZE];
this.currentScore = 0;
this.gameWon = false;
this.gameOver = false;
addNewTile();
addNewTile();
}
// --- Logique du Jeu ---
/**
* Enregistre l'état actuel de la partie (plateau, score) dans les préférences partagées.
* 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.
*/
public void saveGameState() {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
String serializedState = toString(); // Utilise la méthode toString() pour la sérialisation
editor.putString(GAME_STATE_KEY, serializedState);
editor.apply();
}
/**
* Ajoute une nouvelle tuile aléatoire à une position aléatoire vide sur le plateau si possible.
*/
public void addNewTile() { // Renommé
if (!hasEmptyCell()) {
return; // Ne fait rien si le plateau est plein
}
public void addNewTile() {
if (!hasEmptyCell()) { return; }
List<int[]> emptyCells = new ArrayList<>();
// 1. Collecter les coordonnées des cellules vides.
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 (board[row][col] == 0) { emptyCells.add(new int[]{row, col}); }
}
}
// 2. S'il y a des cellules vides, ajouter une nouvelle tuile.
if (!emptyCells.isEmpty()) {
int[] randomCell = emptyCells.get(getRandomNumberGenerator().nextInt(emptyCells.size()));
int row = randomCell[0];
int col = randomCell[1];
int value = generateRandomTileValue(); // Utilise la nouvelle logique de probabilité
setCellValue(row, col, value);
int[] randomCell = emptyCells.get(randomNumberGenerator.nextInt(emptyCells.size()));
int value = generateRandomTileValue();
setCellValue(randomCell[0], randomCell[1], value);
}
}
/**
* Génère une valeur de tuile aléatoire selon les probabilités définies.
*
* @return La valeur de la nouvelle tuile (2, 4, 8, ...) avec les probabilités spécifiées.
* 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.
*/
private int generateRandomTileValue() { // Logique de probabilité modifiée
private int generateRandomTileValue() {
int randomValue = randomNumberGenerator.nextInt(10000);
if (randomValue < 8540) { // 85.40%
return 2;
} else if (randomValue < 9740) { // 12.00%
return 4;
} else if (randomValue < 9940) { // 2.00%
return 8;
} else if (randomValue < 9990) { // 0.50%
return 16;
} else if (randomValue < 9995) { // 0.05%
return 32;
} else if (randomValue < 9998) { // 0.03%
return 64;
} else if (randomValue < 9999) { // 0.01%
return 128;
}else { // 0.01%
return 256;
}
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;
}
/**
* Déplace les tuiles vers le haut, en fusionnant les tuiles de même valeur.
*
* @return True si le plateau a été modifié, False sinon.
* 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.
*/
public boolean pushUp() { // Logique refactorisée, retourne boolean, utilise hasMerged
boolean boardChanged = false;
boolean[] hasMerged = new boolean[BOARD_SIZE];
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]; // Réinitialise par colonne
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;
// 1. Déplacer la tuile
while (currentRow > 0 && getCellValue(currentRow - 1, col) == 0) {
setCellValue(currentRow - 1, col, currentValue);
setCellValue(currentRow, col, 0);
currentRow--;
boardChanged = true;
}
// 2. Fusionner si possible
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); // La tuile d'origine disparaît
setCurrentScore(getCurrentScore() + newValue);
hasMerged[currentRow - 1] = true;
boardChanged = true;
updateHighestScore(); // Met à jour le meilleur score si nécessaire
// checkWinCondition(); // Vérifie si la victoire est atteinte
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(); // Vérifie après tous les mouvements de la colonne
checkGameOverCondition(); // Vérifie si la partie est terminée
return boardChanged;
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
/**
* Déplace les tuiles vers le bas, en fusionnant les tuiles de même valeur.
*
* @return True si le plateau a été modifié, False sinon.
* Tente de déplacer et fusionner les tuiles vers le BAS.
* @return true si changement, false sinon.
*/
public boolean pushDown() { // Logique refactorisée
boolean boardChanged = false;
boolean[] hasMerged = new boolean[BOARD_SIZE];
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--) { // Itère de bas en haut
for (int row = BOARD_SIZE - 2; row >= 0; row--) {
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col);
int currentRow = row;
// Déplacer vers le bas
while (currentRow < BOARD_SIZE - 1 && getCellValue(currentRow + 1, col) == 0) {
setCellValue(currentRow + 1, col, currentValue);
setCellValue(currentRow, col, 0);
currentRow++;
boardChanged = true;
}
// Fusionner si possible
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);
setCurrentScore(getCurrentScore() + newValue);
hasMerged[currentRow + 1] = true;
boardChanged = true;
updateHighestScore();
// checkWinCondition();
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;
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
/**
* Déplace les tuiles vers la gauche, en fusionnant les tuiles de même valeur.
*
* @return True si le plateau a été modifié, False sinon.
* Tente de déplacer et fusionner les tuiles vers la GAUCHE.
* @return true si changement, false sinon.
*/
public boolean pushLeft() { // Logique refactorisée
boolean boardChanged = false;
boolean[] hasMerged = new boolean[BOARD_SIZE]; // Par ligne cette fois
for (int row = 0; row < BOARD_SIZE; row++) {
hasMerged = new boolean[BOARD_SIZE]; // Réinitialise par ligne
for (int col = 1; col < BOARD_SIZE; col++) { // Commence à la 2ème colonne
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col);
int currentCol = col;
// Déplacer vers la gauche
while (currentCol > 0 && getCellValue(row, currentCol - 1) == 0) {
setCellValue(row, currentCol - 1, currentValue);
setCellValue(row, currentCol, 0);
currentCol--;
boardChanged = true;
}
// Fusionner si possible
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);
setCurrentScore(getCurrentScore() + newValue);
hasMerged[currentCol - 1] = true;
boardChanged = true;
updateHighestScore();
// checkWinCondition();
}
}
}
}
checkWinCondition();
checkGameOverCondition();
return boardChanged;
}
/**
* Déplace les tuiles vers la droite, en fusionnant les tuiles de même valeur.
*
* @return True si le plateau a été modifié, False sinon.
*/
public boolean pushRight() { // Logique refactorisée
boolean boardChanged = false;
boolean[] hasMerged = new boolean[BOARD_SIZE]; // Par ligne
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 = BOARD_SIZE - 2; col >= 0; col--) { // Itère de droite à gauche
for (int col = 1; col < BOARD_SIZE; col++) {
if (getCellValue(row, col) != 0) {
int currentValue = getCellValue(row, col);
int currentCol = col;
// Déplacer vers la droite
while (currentCol < BOARD_SIZE - 1 && getCellValue(row, currentCol + 1) == 0) {
setCellValue(row, currentCol + 1, currentValue);
setCellValue(row, currentCol, 0);
currentCol++;
boardChanged = true;
}
// Fusionner si possible
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);
setCurrentScore(getCurrentScore() + newValue);
hasMerged[currentCol + 1] = true;
boardChanged = true;
updateHighestScore();
// checkWinCondition();
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;
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
/**
* Met à jour le meilleur score si le score actuel le dépasse, et sauvegarde le nouveau meilleur score.
* Tente de déplacer et fusionner les tuiles vers la DROITE.
* @return true si changement, false sinon.
*/
private void updateHighestScore() { // Renommé
if (getCurrentScore() > getHighestScore()) {
setHighestScore(getCurrentScore());
saveHighScore(); // Sauvegarde implémentée
}
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--) {
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;
}
}
}
} checkWinCondition(); checkGameOverCondition(); return boardChanged;
}
/**
* Sérialise l'état du jeu (plateau, score courant, meilleur score) en une chaîne de caractères.
* Format: "row1col1,row1col2,...,row1colN,row2col1,...,rowNcolN,currentScore,highestScore"
*
* @return Une chaîne représentant l'état complet du jeu.
* Sérialise l'état actuel du jeu (plateau et score courant) en une chaîne.
* Format: "val,val,...,val,score"
* @return Chaîne sérialisée.
*/
@NonNull
@Override
public String toString() { // Renommé et Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
sb.append(board[row][col]).append(","); // Utilise board directement
}
for (int col = 0; col < BOARD_SIZE; col++) { sb.append(board[row][col]).append(","); }
}
sb.append(getCurrentScore()).append(",");
sb.append(getHighestScore()); // Sérialise highestScore
sb.append(currentScore);
return sb.toString();
}
/**
* Désérialise une chaîne de caractères pour restaurer l'état du jeu.
*
* @param serializedState La chaîne représentant l'état du jeu (au format de la méthode toString).
* @param context Le contexte Android (nécessaire pour le constructeur de Game).
* @return Un nouvel objet Game restauré, ou null si la désérialisation échoue.
* 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.
*/
public static Game deserialize(String serializedState, Context context) { // Logique améliorée
if (serializedState == null || serializedState.isEmpty()) {
return null;
}
public static Game deserialize(String serializedState) {
if (serializedState == null || serializedState.isEmpty()) return null;
String[] values = serializedState.split(",");
// Vérifie si le nombre d'éléments correspond à la taille attendue (board + score + highScore)
if (values.length != (BOARD_SIZE * BOARD_SIZE + 2)) {
System.err.println("Erreur de désérialisation : nombre d'éléments incorrect. Attendu=" + (BOARD_SIZE * BOARD_SIZE + 2) + ", Obtenu=" + values.length);
return null; // Longueur incorrecte
}
int[][] newBoard = new int[BOARD_SIZE][BOARD_SIZE];
int index = 0;
if (values.length != (BOARD_SIZE * BOARD_SIZE + 1)) return null;
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++]);
}
for (int col = 0; col < BOARD_SIZE; col++) { newBoard[row][col] = Integer.parseInt(values[index++]); }
}
int score = Integer.parseInt(values[index++]);
int highScore = Integer.parseInt(values[index++]); // Désérialise highScore
// Utilise le constructeur qui prend le contexte
return new Game(newBoard, score, highScore, context);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
System.err.println("Erreur de désérialisation : " + e.getMessage());
// e.printStackTrace(); // Optionnel: pour plus de détails dans Logcat
return null; // Erreur de format ou index hors limites
}
int score = Integer.parseInt(values[index]);
return new Game(newBoard, score);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { return null; }
}
/**
* Vérifie si la condition de victoire (tuile 2048) est remplie.
* Met à jour la variable gameWon.
* Vérifie si une tuile >= 2048 existe sur le plateau et met à jour l'état `gameWon`.
*/
private void checkWinCondition() { // Ajouté
// Ne vérifie que si la partie n'est pas déjà gagnée
if (!isGameWon()) {
for(int row = 0 ; row < BOARD_SIZE; row++){
for (int col = 0; col < BOARD_SIZE; col++){
if(getCellValue(row, col) >= 2048){ // Condition >= 2048
setGameWon(true);
// Optionnel : On pourrait arrêter la vérification ici
// return;
}
}
}
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; }
}
}
/**
* Vérifie si la condition de fin de partie (aucun mouvement possible) est remplie.
* Met à jour la variable gameOver.
* Vérifie s'il reste des mouvements possibles (case vide ou fusion adjacente).
* Met à jour l'état `gameOver`.
*/
private void checkGameOverCondition() { // Ajouté
// Si une case est vide, la partie n'est pas terminée
if (hasEmptyCell()) {
setGameOver(false);
return;
}
// Vérifie les fusions possibles horizontalement et verticalement
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
int currentValue = getCellValue(row, col);
// Vérifie voisin du haut
if (row > 0 && getCellValue(row - 1, col) == currentValue) {
setGameOver(false);
return;
}
// Vérifie voisin du bas
if (row < BOARD_SIZE - 1 && getCellValue(row + 1, col) == currentValue) {
setGameOver(false);
return;
}
// Vérifie voisin de gauche
if (col > 0 && getCellValue(row, col - 1) == currentValue) {
setGameOver(false);
return;
}
// Vérifie voisin de droite
if (col < BOARD_SIZE - 1 && getCellValue(row, col + 1) == currentValue) {
setGameOver(false);
return;
}
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++) {
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
}
}
// Si aucune case vide et aucune fusion possible, la partie est terminée
setGameOver(true);
setGameOver(true); // Aucune case vide et aucune fusion -> game over
}
/**
* Vérifie s'il existe au moins une cellule vide sur le plateau.
*
* @return true s'il y a une cellule vide, false sinon.
* @return true s'il y a au moins une case vide sur le plateau, false sinon.
*/
private boolean hasEmptyCell() { // Ajouté
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (getCellValue(row, col) == 0) {
return true;
}
}
}
private boolean hasEmptyCell() {
for(int r=0; r<BOARD_SIZE; r++) for(int c=0; c<BOARD_SIZE; c++) if(getCellValue(r,c)==0) return true;
return false;
}
/**
* Trouve et retourne la valeur de la tuile la plus élevée actuellement sur le plateau.
* @return La valeur maximale trouvée.
*/
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];
return maxTile;
}
}

View File

@ -0,0 +1,274 @@
// 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.
*/
package legion.muyue.best2048;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.TimeUnit;
public class GameStats {
// Clés SharedPreferences (inchangées)
private static final String PREFS_NAME = "Best2048_Prefs";
private static final String HIGH_SCORE_KEY = "high_score";
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";
private static final String STATS_TOTAL_MERGES = "totalMerges";
private static final String STATS_HIGHEST_TILE = "highestTile";
private static final String STATS_OBJECTIVE_REACHED_COUNT = "numberOfTimesObjectiveReached";
private static final String STATS_PERFECT_GAMES = "perfectGames";
private static final String STATS_BEST_WINNING_TIME_MS = "bestWinningTimeMs";
private static final String STATS_WORST_WINNING_TIME_MS = "worstWinningTimeMs";
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";
private static final String STATS_MP_TOTAL_SCORE = "multiplayerTotalScore";
private static final String STATS_MP_TOTAL_TIME_MS = "multiplayerTotalTimeMs";
private static final String STATS_MP_LOSSES = "totalMultiplayerLosses";
private static final String STATS_MP_HIGH_SCORE = "multiplayerHighScore";
// Champs statistiques (inchangés)
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 long bestWinningTimeMs;
private long worstWinningTimeMs;
private int multiplayerGamesWon;
private int multiplayerGamesPlayed;
private int multiplayerBestWinningStreak;
private long multiplayerTotalScore;
private long multiplayerTotalTimeMs;
private int totalMultiplayerLosses;
private int multiplayerHighestScore;
private int overallHighScore;
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).
*/
public GameStats(Context context) {
this.context = context;
loadStats();
}
/**
* Charge toutes les statistiques (générales et multijoueur) et le high score global
* depuis les SharedPreferences.
*/
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);
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);
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);
multiplayerBestWinningStreak = prefs.getInt(STATS_MP_BEST_WINNING_STREAK, 0);
multiplayerTotalScore = prefs.getLong(STATS_MP_TOTAL_SCORE, 0);
multiplayerTotalTimeMs = prefs.getLong(STATS_MP_TOTAL_TIME_MS, 0);
totalMultiplayerLosses = prefs.getInt(STATS_MP_LOSSES, 0);
multiplayerHighestScore = prefs.getInt(STATS_MP_HIGH_SCORE, 0);
}
/**
* Sauvegarde toutes les statistiques (générales et multijoueur) et le high score global
* dans les SharedPreferences.
*/
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(STATS_TOTAL_GAMES_PLAYED, totalGamesPlayed);
editor.putInt(STATS_TOTAL_GAMES_STARTED, totalGamesStarted);
editor.putInt(STATS_TOTAL_MOVES, totalMoves);
editor.putLong(STATS_TOTAL_PLAY_TIME_MS, totalPlayTimeMs);
editor.putInt(STATS_TOTAL_MERGES, totalMerges);
editor.putInt(STATS_HIGHEST_TILE, highestTile);
editor.putInt(STATS_OBJECTIVE_REACHED_COUNT, numberOfTimesObjectiveReached);
editor.putInt(STATS_PERFECT_GAMES, perfectGames);
editor.putLong(STATS_BEST_WINNING_TIME_MS, bestWinningTimeMs);
editor.putLong(STATS_WORST_WINNING_TIME_MS, worstWinningTimeMs);
editor.putInt(STATS_MP_GAMES_WON, multiplayerGamesWon);
editor.putInt(STATS_MP_GAMES_PLAYED, multiplayerGamesPlayed);
editor.putInt(STATS_MP_BEST_WINNING_STREAK, multiplayerBestWinningStreak);
editor.putLong(STATS_MP_TOTAL_SCORE, multiplayerTotalScore);
editor.putLong(STATS_MP_TOTAL_TIME_MS, multiplayerTotalTimeMs);
editor.putInt(STATS_MP_LOSSES, totalMultiplayerLosses);
editor.putInt(STATS_MP_HIGH_SCORE, multiplayerHighestScore);
editor.apply();
}
/**
* 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.
*/
public void startGame() {
totalGamesStarted++;
currentMoves = 0;
mergesThisGame = 0;
currentGameStartTimeMs = System.currentTimeMillis();
}
/**
* Enregistre un mouvement effectué pendant la partie en cours.
* Incrémente le compteur de mouvements de la partie et le compteur total.
*/
public void recordMove() {
currentMoves++;
totalMoves++;
}
/**
* Enregistre une ou plusieurs fusions survenues pendant la partie en cours.
* @param numberOfMerges Le nombre de fusions à ajouter (idéalement précis).
*/
public void recordMerge(int numberOfMerges) {
if (numberOfMerges > 0) {
mergesThisGame += numberOfMerges;
totalMerges += numberOfMerges;
}
}
/**
* 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.
*/
public void updateHighestTile(int tileValue) {
if (tileValue > this.highestTile) {
this.highestTile = tileValue;
}
}
/**
* 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.
*/
public void recordWin(long timeTakenMs) {
numberOfTimesObjectiveReached++;
endGame(timeTakenMs); // Finalise les stats de la partie
if (timeTakenMs < bestWinningTimeMs) { bestWinningTimeMs = timeTakenMs; }
if (timeTakenMs > worstWinningTimeMs) { worstWinningTimeMs = timeTakenMs; }
}
/**
* Enregistre une défaite et finalise les statistiques de la partie.
*/
public void recordLoss() {
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.
*/
public void endGame(long timeTakenMs) {
totalGamesPlayed++;
addPlayTime(timeTakenMs);
}
/**
* Ajoute une durée au temps de jeu total cumulé.
* @param durationMs Durée à ajouter en millisecondes.
*/
public void addPlayTime(long durationMs) {
if (durationMs > 0) {
this.totalPlayTimeMs += durationMs;
}
}
// --- Getters ---
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 int getMergesThisGame() { return mergesThisGame; }
public int getTotalMerges() { return totalMerges; }
public int getHighestTile() { return highestTile; }
public int getNumberOfTimesObjectiveReached() { return numberOfTimesObjectiveReached; }
public int getPerfectGames() { return perfectGames; }
public long getBestWinningTimeMs() { return bestWinningTimeMs; }
public long getWorstWinningTimeMs() { return worstWinningTimeMs; }
public int getMultiplayerGamesWon() { return multiplayerGamesWon; }
public int getMultiplayerGamesPlayed() { return multiplayerGamesPlayed; }
public int getMultiplayerBestWinningStreak() { return multiplayerBestWinningStreak; }
public long getMultiplayerTotalScore() { return multiplayerTotalScore; }
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.
*/
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;
}
/**
* 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 ---
public long getAverageGameTimeMs() { return (totalGamesPlayed > 0) ? totalPlayTimeMs / totalGamesPlayed : 0; }
public int getMultiplayerAverageScore() { return (multiplayerGamesPlayed > 0) ? (int)(multiplayerTotalScore / multiplayerGamesPlayed) : 0; }
public long getMultiplayerAverageTimeMs() { return (multiplayerGamesPlayed > 0) ? multiplayerTotalTimeMs / multiplayerGamesPlayed : 0; }
/**
* Formate une durée en millisecondes en chaîne "hh:mm:ss" ou "mm:ss".
* @param milliseconds Durée en millisecondes.
* @return Chaîne de temps formatée.
*/
public static String formatTime(long milliseconds) {
long hours = TimeUnit.MILLISECONDS.toHours(milliseconds);
long minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60;
long seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60;
if (hours > 0) { return String.format("%02d:%02d:%02d", hours, minutes, seconds); }
else { return String.format("%02d:%02d", minutes, seconds); }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -143,7 +143,6 @@ public class OnSwipeTouchListener implements View.OnTouchListener {
}
}
} catch (Exception exception) {
// exception.printStackTrace(); // Commenté dans le nouveau code ? Gardons commenté.
exception.fillInStackTrace(); // Gestion des erreurs (journalisation).
}
return result;