From 73ab81e20832b88faa375b93c054752b092bf106 Mon Sep 17 00:00:00 2001 From: Muyue Date: Fri, 4 Apr 2025 12:06:08 +0200 Subject: [PATCH] Feat: Stats, Refactor, Permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AndroidManifest: Ajout permissions (Network, BT, Location). - Game.java: Refonte majeure (renommage, contexte, états win/loss, logique SharedPreferences implémentée, probabilités addNewTile modifiées, méthodes check win/loss, pushX retourne boolean, JavaDoc). - MainActivity.java: Refonte majeure (gestion stats via ViewStub, champs/méthodes stats, dialogs win/loss, refactorisation onCreate, getters/setters, gestion cycle vie onPause/onResume, MAJ handleSwipe). - OnSwipeTouchListener.java: Ajout annotations, JavaDoc. - Layouts: Ajout stats_layout.xml, ajout ViewStub dans activity_main.xml. - Ressources: Ajout/MAJ strings (stats), colors (thème, tuiles, stats), styles (stats, fullscreen), dimens, anim (durée), drawables (bouton multi, tile_background). --- app/src/main/AndroidManifest.xml | 10 + .../main/java/legion/muyue/best2048/Game.java | 707 +++++++++++++--- .../legion/muyue/best2048/MainActivity.java | 772 ++++++++++++++---- .../muyue/best2048/OnSwipeTouchListener.java | 118 ++- app/src/main/res/anim/button_press.xml | 13 +- app/src/main/res/anim/button_release.xml | 13 +- .../button_multiplayer_background.xml | 39 +- .../main/res/drawable/dialog_background.xml | 4 +- app/src/main/res/drawable/tile_background.xml | 10 +- app/src/main/res/layout/activity_main.xml | 28 +- app/src/main/res/layout/stats_layout.xml | 270 ++++++ app/src/main/res/values/colors.xml | 51 +- app/src/main/res/values/strings.xml | 31 + app/src/main/res/values/styles.xml | 41 +- 14 files changed, 1726 insertions(+), 381 deletions(-) create mode 100644 app/src/main/res/layout/stats_layout.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a11c4e9..3185668 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,16 @@ + + + + + + + + + + = BOARD_SIZE || column < 0 || column >= BOARD_SIZE) { + throw new IllegalArgumentException("Indices de ligne ou de colonne hors limites : row=" + row + ", column=" + column); + } + return this.board[row][column]; } - public int getHighScore() { - return highScore; + /** + * 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. + */ + 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); + } + this.board[row][col] = value; } - // Ajout Setter highScore - public void setHighScore(int highScore) { - this.highScore = highScore; + /** + * Récupère le score actuel de la partie. + * + * @return Le score actuel. + */ + public int getCurrentScore() { + return this.currentScore; } + /** + * Met à jour le score actuel. + * + * @param currentScore Le nouveau score actuel. + */ + public void setCurrentScore(int currentScore) { + this.currentScore = currentScore; + } + + /** + * Récupère le meilleur score enregistré. + * + * @return Le meilleur score. + */ + public int getHighestScore() { + return this.highestScore; + } + + /** + * 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; + } + + /** + * Met à jour le board. Utilisé lors du chargement. + * + * @param board Le nouveau board. + */ + public void setBoard(int[][] board){ + this.board = board; + } + + /** + * 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; + } + + /** + * 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() { - // highScore = sharedPreferences.getInt("high_score", 0); // Stub + 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.Editor editor = sharedPreferences.edit(); - editor.putInt("high_score", highScore); - editor.apply(); */ // Stub + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(HIGH_SCORE_KEY, getHighestScore()); + editor.apply(); } - public void addNewNumbers() { - List emptySpaces = new ArrayList<>(); - - for (int x = 0; x < 4; x++) { - for (int y = 0; y < 4; y++) { - if (gameBoard[x][y] == 0) { - emptySpaces.add(new int[]{x, y}); - } - } - } - - if (!emptySpaces.isEmpty()) { - int choice = random.nextInt(emptySpaces.size()); - int[] coordinates = emptySpaces.get(choice); - int x = coordinates[0]; - int y = coordinates[1]; - int newNumber; - int randomNumber = random.nextInt(100); - - if (randomNumber < 85) { - newNumber = 2; - } else if (randomNumber < 95) { - newNumber = 4; + /** + * 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 { - newNumber = 8; + // Si l'état sauvegardé est invalide, commence une nouvelle partie + initializeNewBoard(); } - - gameBoard[x][y] = newNumber; + } else { + // Si aucun état n'est sauvegardé, commence une nouvelle partie + initializeNewBoard(); } } - public void pushUp() { - Log.d("Game", "Pushing Up"); - boolean[] alreadyCombined = new boolean[4]; - for (int y = 0; y < 4; y++) { - alreadyCombined = new boolean[4]; - for (int x = 1; x < 4; x++) { - if (gameBoard[x][y] != 0) { - int value = gameBoard[x][y]; int currentX = x; - while (currentX > 0 && gameBoard[currentX - 1][y] == 0) { currentX--; } - if (currentX == 0) { gameBoard[0][y] = value; if (currentX != x) gameBoard[x][y] = 0; } - else if (gameBoard[currentX - 1][y] != value) { gameBoard[currentX][y] = value; if (currentX != x) gameBoard[x][y] = 0; } - else if (!alreadyCombined[currentX - 1]) { gameBoard[currentX - 1][y] *= 2; score += gameBoard[currentX - 1][y]; gameBoard[x][y] = 0; alreadyCombined[currentX - 1] = true; updateHighScore(); } - else { gameBoard[currentX][y] = value; if (currentX != x) gameBoard[x][y] = 0; } + /** + * Initialise le plateau pour une nouvelle partie (typiquement avec deux tuiles). + */ + private void initializeNewBoard() { + this.board = new int[BOARD_SIZE][BOARD_SIZE]; // Assure que le plateau est vide + this.currentScore = 0; + addNewTile(); + addNewTile(); + } + + + /** + * Enregistre l'état actuel de la partie (plateau, score) dans les préférences partagées. + */ + 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 + } + + List 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}); } } } + + // 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); + } } - public void pushDown() { - Log.d("Game", "Pushing Down"); - boolean[] alreadyCombined = new boolean[4]; - for (int y = 0; y < 4; y++) { - alreadyCombined = new boolean[4]; - for (int x = 2; x >= 0; x--) { - if (gameBoard[x][y] != 0) { - int value = gameBoard[x][y]; int currentX = x; - while (currentX < 3 && gameBoard[currentX + 1][y] == 0) { currentX++; } - if (currentX == 3) { gameBoard[3][y] = value; if(currentX != x) gameBoard[x][y] = 0; } - else if (gameBoard[currentX + 1][y] != value) { gameBoard[currentX][y] = value; if(currentX != x) gameBoard[x][y] = 0; } - else if (!alreadyCombined[currentX + 1]) { gameBoard[currentX + 1][y] *= 2; score += gameBoard[currentX + 1][y]; gameBoard[x][y] = 0; alreadyCombined[currentX + 1] = true; updateHighScore(); } - else { gameBoard[currentX][y] = value; if (currentX != x) gameBoard[x][y] = 0; } + /** + * 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. + */ + private int generateRandomTileValue() { // Logique de probabilité modifiée + 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; + } + } + + /** + * 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. + */ + public boolean pushUp() { // Logique refactorisée, retourne boolean, utilise hasMerged + 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 + 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 + 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 + } } } } + checkWinCondition(); // Vérifie après tous les mouvements de la colonne + checkGameOverCondition(); // Vérifie si la partie est terminée + return boardChanged; } - public void pushLeft() { - Log.d("Game", "Pushing Left"); - boolean[] alreadyCombined = new boolean[4]; - for (int x = 0; x < 4; x++) { - alreadyCombined = new boolean[4]; - for (int y = 1; y < 4; y++) { - if (gameBoard[x][y] != 0) { - int value = gameBoard[x][y]; int currentY = y; - while (currentY > 0 && gameBoard[x][currentY - 1] == 0) { currentY--; } - if (currentY == 0) { gameBoard[x][0] = value; if(currentY != y) gameBoard[x][y] = 0; } - else if (gameBoard[x][currentY - 1] != value) { gameBoard[x][currentY] = value; if(currentY != y) gameBoard[x][y] = 0; } - else if (!alreadyCombined[currentY - 1]) { gameBoard[x][currentY - 1] *= 2; score += gameBoard[x][currentY - 1]; gameBoard[x][y] = 0; alreadyCombined[currentY - 1] = true; updateHighScore(); } - else { gameBoard[x][currentY] = value; if(currentY != y) gameBoard[x][y] = 0; } + + /** + * 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. + */ + public boolean pushDown() { // Logique refactorisée + 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 + 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 + 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(); + } } } } + checkWinCondition(); + checkGameOverCondition(); + return boardChanged; } - public void pushRight() { - Log.d("Game", "Pushing Right"); - boolean[] alreadyCombined = new boolean[4]; - for (int x = 0; x < 4; x++) { - alreadyCombined = new boolean[4]; - for (int y = 2; y >= 0; y--) { - if (gameBoard[x][y] != 0) { - int value = gameBoard[x][y]; int currentY = y; - while (currentY < 3 && gameBoard[x][currentY + 1] == 0) { currentY++; } - if (currentY == 3) { gameBoard[x][3] = value; if (currentY != y) gameBoard[x][y] = 0; } - else if (gameBoard[x][currentY + 1] != value) { gameBoard[x][currentY] = value; if(currentY != y) gameBoard[x][y] = 0; } - else if (!alreadyCombined[currentY + 1]) { gameBoard[x][currentY + 1] *= 2; score += gameBoard[x][currentY + 1]; gameBoard[x][y] = 0; alreadyCombined[currentY + 1] = true; updateHighScore(); } - else { gameBoard[x][currentY] = value; if (currentY != y) gameBoard[x][y] = 0; } + /** + * 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. + */ + 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; } - private void updateHighScore() { - if (score > highScore) { - highScore = score; - saveHighScore(); // Appel stub + /** + * 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 + + 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 + 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(); + } + } + } + } + checkWinCondition(); + checkGameOverCondition(); + return boardChanged; + } + + + /** + * Met à jour le meilleur score si le score actuel le dépasse, et sauvegarde le nouveau meilleur score. + */ + private void updateHighestScore() { // Renommé + if (getCurrentScore() > getHighestScore()) { + setHighestScore(getCurrentScore()); + saveHighScore(); // Sauvegarde implémentée } } - // Nouvelle méthode Sérialisation - public String serialize() { + /** + * 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. + */ + @NonNull + @Override + public String toString() { // Renommé et Override StringBuilder sb = new StringBuilder(); - for (int x = 0; x < 4; x++) { - for (int y = 0; y < 4; y++) { - sb.append(gameBoard[x][y]).append(","); + + 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 } } - sb.append(score).append(","); - sb.append(highScore); // Ajout highScore + + sb.append(getCurrentScore()).append(","); + sb.append(getHighestScore()); // Sérialise highestScore + return sb.toString(); } - // Nouvelle méthode Désérialisation - public static Game deserialize(String serializedData) { - String[] data = serializedData.split(","); - int[][] board = new int[4][4]; + + /** + * 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. + */ + public static Game deserialize(String serializedState, Context context) { // Logique améliorée + 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; - for (int x = 0; x < 4; x++) { - for (int y = 0; y < 4; y++) { - board[x][y] = Integer.parseInt(data[index++]); + + try { + for (int row = 0; row < BOARD_SIZE; row++) { + for (int col = 0; col < BOARD_SIZE; col++) { + newBoard[row][col] = Integer.parseInt(values[index++]); + } + } + + int score = Integer.parseInt(values[index++]); + int 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 + } + } + + + /** + * Vérifie si la condition de victoire (tuile 2048) est remplie. + * Met à jour la variable 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; + } + } } } - int score = Integer.parseInt(data[index++]); - int highScore = Integer.parseInt(data[index++]); // Lecture highScore - - return new Game(board, score, highScore); // Appel nouveau constructeur } - // printArray reste défini - public void printArray() { - for (int[] row : gameBoard) { - String rowString = String.format("%6d%6d%6d%6d%n", row[0], row[1], row[2], row[3]); - Log.d("Game", rowString); + /** + * Vérifie si la condition de fin de partie (aucun mouvement possible) est remplie. + * Met à jour la variable gameOver. + */ + private void checkGameOverCondition() { // Ajouté + // Si une case est vide, la partie n'est pas terminée + if (hasEmptyCell()) { + setGameOver(false); + return; } - Log.d("Game", "\n"); + + // 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; + } + } + } + + // Si aucune case vide et aucune fusion possible, la partie est terminée + setGameOver(true); } + + /** + * Vérifie s'il existe au moins une cellule vide sur le plateau. + * + * @return true s'il y a une cellule vide, 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; + } + } + } + return false; + } + } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index 75e385d..03b9052 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -1,12 +1,45 @@ +// Fichier MainActivity.java +// C'est l'activité principale de l'application, l'écran principal avec lequel l'utilisateur interagit. +/* + Fonctions principales : + - Gère l'interface utilisateur : + - Initialise le GridLayout (gameBoardLayout). + - Met à jour les TextView pour le score (scoreTextView) et le meilleur score (highScoreTextView). + - updateUI() : Redessine la grille en fonction de l'état de l'objet Game. + - setTileAppearance() : Applique le style visuel (couleur, taille du texte) aux tuiles. + - Gère les entrées utilisateur (swipes) via OnSwipeTouchListener. + - Gère les clics sur les boutons (Recommencer, Statistiques, Menu, Multijoueur). + - Affiche des boîtes de dialogue (confirmation de redémarrage, statistiques). + - Gère le cycle de vie de l'activité : + - onCreate() : Initialisation. + - onResume() : Reprise de l'application. + - onPause() : Sauvegarde de l'état et des statistiques. + - Gère la persistance des données (SharedPreferences) : état du jeu, meilleur score, statistiques. + - showStats(), hideStats() : Gère l'affichage/masquage des statistiques (ViewStub). + - Gère les statistiques du jeu. + + Relations : + - Game : Interagit constamment avec l'objet Game. + - OnSwipeTouchListener : Détection des swipes. + - res/layout/*.xml : Utilise les fichiers de layout pour l'interface utilisateur. + - res/drawable/*.xml : Apparence des tuiles et boutons. + - res/values/*.xml : Couleurs, dimensions, chaînes, styles. + - res/anim/*.xml : Animation du bouton multijoueur. + - SharedPreferences : Sauvegarde et chargement des données. +*/ + package legion.muyue.best2048; -import android.app.AlertDialog; // Ajout -import android.content.SharedPreferences; // Ajout +import android.annotation.SuppressLint; // Ajout pour ClickableViewAccessibility +import android.app.AlertDialog; +import android.content.Context; // Non utilisé directement mais ok +import android.content.SharedPreferences; import android.os.Bundle; import android.util.TypedValue; import android.view.Gravity; -import android.view.LayoutInflater; // Ajout -import android.view.View; // Ajout +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; // Ajout pour ViewStub import android.view.animation.AnimationUtils; import android.widget.TextView; @@ -19,17 +52,52 @@ import android.widget.Button; public class MainActivity extends AppCompatActivity { - private GridLayout gameBoardLayout; - private TextView scoreTextView; - private TextView highScoreTextView; + // UI Elements (renommés/ajoutés) + private GridLayout boardGridLayout; + private TextView currentScoreTextView; + private TextView highestScoreTextView; + private Button newGameButton; + private Button multiplayerButton; + private Button statisticsButton; + private Button menuButton; + private ViewStub statisticsViewStub; // Ajout pour stats + + // Game Logic private Game game; - private SharedPreferences sharedPreferences; // Ajout + private static final int BOARD_SIZE = 4; // Ajout constante - // Constantes SharedPreferences - private static final String PREFS_NAME = "MyPrefs"; - private static final String GAME_STATE_KEY = "gameState"; - private static final String HIGH_SCORE_KEY = "highScore"; + // Preferences + private SharedPreferences preferences; // Renommé + // Constantes déjà définies dans Game, redéfinies ici ? Ok pour l'instant. + private static final String PREFS_NAME = "Best2048_Prefs"; + private static final String HIGH_SCORE_KEY = "high_score"; + private static final String GAME_STATE_KEY = "game_state"; + // Statistics (nouveaux champs) + private boolean statisticsVisible = false; + private int totalGamesPlayed = 0; + private int totalGamesStarted = 0; + private int totalMoves = 0; + private int currentMoves = 0; + private long totalPlayTimeMs = 0; + private long currentGameStartTimeMs = 0; + private int mergesThisGame = 0; + private int totalMerges = 0; + private int highestTile = 0; + private int numberOfTimesObjectiveReached = 0; + private int perfectGames = 0; + private long averageGameTimeMs = 0; + private long bestWinningTimeMs = Long.MAX_VALUE; + private long worstWinningTimeMs = 0; + + // Multiplayer Statistics (nouveaux champs) + private int multiplayerGamesWon = 0; + private int multiplayerGamesPlayed = 0; + private int multiplayerBestWinningStreak = 0; + private int multiplayerAverageScore = 0; + private long multiplayerAverageGameTimeMs = 0; + private int totalMultiplayerLosses = 0; + private int multiplayerHighestScore = 0; @Override protected void onCreate(Bundle savedInstanceState) { @@ -37,167 +105,241 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - gameBoardLayout = findViewById(R.id.gameBoard); - scoreTextView = findViewById(R.id.scoreLabel); - highScoreTextView = findViewById(R.id.highScoreLabel); - Button restartButton = findViewById(R.id.restartButton); - Button statsButton = findViewById(R.id.statsButton); - Button menuButton = findViewById(R.id.menuButton); - Button multiplayerButton = findViewById(R.id.multiplayerButton); + findViews(); // Refactorisation onCreate + initializeGame(); // Refactorisation onCreate + setupListeners(); // Refactorisation onCreate + } - // Initialisation SharedPreferences - sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + // Getters/Setters ajoutés (pour UI et Game) + public void setBoardGridLayout(GridLayout boardGridLayout) { this.boardGridLayout = boardGridLayout; } + public void setCurrentScoreTextView(TextView currentScoreTextView) { this.currentScoreTextView = currentScoreTextView; } + public void setHighestScoreTextView(TextView highestScoreTextView) { this.highestScoreTextView = highestScoreTextView; } + public void setNewGameButton(Button newGameButton) { this.newGameButton = newGameButton; } + public void setMultiplayerButton(Button multiplayerButton) { this.multiplayerButton = multiplayerButton; } + public void setStatisticsButton(Button statisticsButton) { this.statisticsButton = statisticsButton; } + public void setMenuButton(Button menuButton) { this.menuButton = menuButton; } + public void setStatisticsViewStub(ViewStub statisticsViewStub) { this.statisticsViewStub = statisticsViewStub; } + public void setPreferences(SharedPreferences preferences) { this.preferences = preferences; } + public void setGame(Game game) { this.game = game; } + public Button getNewGameButton() { return this.newGameButton; } + public Button getMultiplayerButton() { return this.multiplayerButton; } + public Button getStatisticsButton() { return this.statisticsButton; } + public Button getMenuButton() { return this.menuButton; } + public GridLayout getBoardGridLayout() { return this.boardGridLayout; } + public Game getGame() { return this.game; } + public TextView getCurrentScoreTextView() { return this.currentScoreTextView; } + public TextView getHighestScoreTextView() { return this.highestScoreTextView; } - initGameBoardLayout(); + /** + * Initialise les références aux éléments de l'interface utilisateur (UI). + */ + private void findViews() { // Nouvelle méthode + setBoardGridLayout(findViewById(R.id.gameBoard)); + setCurrentScoreTextView(findViewById(R.id.scoreLabel)); + setHighestScoreTextView(findViewById(R.id.highScoreLabel)); + setNewGameButton(findViewById(R.id.restartButton)); // ID reste restartButton + setStatisticsButton(findViewById(R.id.statsButton)); // ID reste statsButton + setMenuButton(findViewById(R.id.menuButton)); // ID reste menuButton + setMultiplayerButton(findViewById(R.id.multiplayerButton)); + setStatisticsViewStub(findViewById(R.id.statsViewStub)); // Ajout ViewStub + } - // Chargement du jeu (ou création si pas de sauvegarde) - loadGame(); + /** + * Initialise l'objet Game, charge le meilleur score et restaure l'état de la partie (si disponible). + */ + private void initializeGame() { // Nouvelle méthode + setPreferences(getSharedPreferences(PREFS_NAME, MODE_PRIVATE)); + setGame(new Game(this)); // Passe le contexte à Game + // loadGame(); // Est appelé implicitement par le constructeur de Game via loadGameState + updateUI(); // MAJ initiale de l'UI + loadStatistics(); // Charge les stats + // Initialise le timer pour la partie chargée ou nouvelle + currentGameStartTimeMs = System.currentTimeMillis(); // Démarre timer ici + totalGamesStarted++; // Incrémente car une partie commence (nouvelle ou chargée) + } - setupSwipeListener(); - - //Listeners des boutons - multiplayerButton.setOnClickListener(v -> { + /** + * Configure les listeners pour les interactions de l'utilisateur (boutons, swipes). + */ + private void setupListeners() { // Nouvelle méthode (contenu déplacé) + getNewGameButton().setOnClickListener(v -> { v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); - showMultiplayerScreen(); + // Appelle maintenant showRestartConfirmationDialog au lieu de startNewGame directement + showRestartConfirmationDialog(); }); - // restartGame appelle maintenant la dialog - restartButton.setOnClickListener(v -> restartGame()); - statsButton.setOnClickListener(v -> showStats()); - menuButton.setOnClickListener(v -> showMenu()); + + getStatisticsButton().setOnClickListener(v -> { + v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); + showStatistics(); // Appelle la nouvelle méthode pour les stats + }); + + getMenuButton().setOnClickListener(v -> { + v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); + showMenu(); // Appelle le stub de menu + }); + + getMultiplayerButton().setOnClickListener(v -> { + v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.button_press)); + showMultiplayerScreen(); // Appelle le stub multijoueur + }); + + setupSwipeListener(); // Configure le swipe listener } - private void initGameBoardLayout() { - gameBoardLayout.removeAllViews(); - gameBoardLayout.setColumnCount(4); - gameBoardLayout.setRowCount(4); + /** + * Initialise le GridLayout qui représente le plateau de jeu. Définit le nombre de lignes et colonnes. + */ + private void initGameBoardLayout() { // Inchangé mais utilise getter + getBoardGridLayout().removeAllViews(); + getBoardGridLayout().setColumnCount(BOARD_SIZE); + getBoardGridLayout().setRowCount(BOARD_SIZE); } - private void updateUI() { - gameBoardLayout.removeAllViews(); + /** + * Met à jour l'ensemble de l'interface utilisateur : le plateau, le score actuel et le meilleur score. + */ + private void updateUI() { // Refactorisé + updateBoard(); + updateScores(); + } - for (int x = 0; x < 4; x++) { - for (int y = 0; y < 4; y++) { + /** + * Met à jour l'affichage des tuiles sur le plateau de jeu. + */ + private void updateBoard() { // Nouvelle méthode (contenu de l'ancien updateUI partie grille) + getBoardGridLayout().removeAllViews(); + + for (int row = 0; row < BOARD_SIZE; row++) { + for (int col = 0; col < BOARD_SIZE; col++) { TextView tileTextView = new TextView(this); - int value = game.getGameBoard(x, y); + int value = getGame().getCellValue(row, col); // Utilise getter renommé - setTileAppearance(tileTextView, value); + setTileStyle(tileTextView, value); // Utilise méthode renommée GridLayout.LayoutParams params = new GridLayout.LayoutParams(); params.width = 0; params.height = 0; - params.rowSpec = GridLayout.spec(x, 1f); - params.columnSpec = GridLayout.spec(y, 1f); - params.setMargins( - (int) getResources().getDimension(R.dimen.tile_margin), - (int) getResources().getDimension(R.dimen.tile_margin), - (int) getResources().getDimension(R.dimen.tile_margin), - (int) getResources().getDimension(R.dimen.tile_margin) - ); + params.rowSpec = GridLayout.spec(row, 1f); + params.columnSpec = GridLayout.spec(col, 1f); + + int margin = (int) getResources().getDimension(R.dimen.tile_margin); + params.setMargins(margin, margin, margin, margin); + tileTextView.setLayoutParams(params); - gameBoardLayout.addView(tileTextView); + getBoardGridLayout().addView(tileTextView); } } - scoreTextView.setText(getString(R.string.score_placeholder, game.getScore())); - highScoreTextView.setText(getString(R.string.high_score_placeholder, game.getHighScore())); } - private void setTileAppearance(TextView tile, int value) { + /** + * Met à jour les TextViews du score actuel et du meilleur score. + */ + private void updateScores() { // Nouvelle méthode (contenu de l'ancien updateUI partie score) + getCurrentScoreTextView().setText(getString(R.string.score_placeholder, getGame().getCurrentScore())); + getHighestScoreTextView().setText(getString(R.string.high_score_placeholder, getGame().getHighestScore())); + } + + + /** + * Définit l'apparence d'une tuile (couleur de fond, couleur du texte, taille du texte) + * en fonction de sa valeur. Utilise un seul Drawable avec des teintes de couleur. + * + * @param tileTextView La TextView représentant la tuile. + * @param value La valeur de la tuile (0, 2, 4, 8, ...). + */ + private void setTileStyle(TextView tileTextView, int value) { // Renommé et logique couleur texte corrigée + + tileTextView.setText(value > 0 ? String.valueOf(value) : ""); + tileTextView.setGravity(Gravity.CENTER); + tileTextView.setTypeface(null, android.graphics.Typeface.BOLD); // Ajout police grasse + + int backgroundColorId; int textColorId; int textSizeId; switch (value) { + case 0: + backgroundColorId = R.color.tile_empty; + textColorId = android.R.color.transparent; + textSizeId = R.dimen.text_size_tile_small; + break; case 2: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_2_color)); + backgroundColorId = R.color.tile_2; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break; case 4: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_4_color)); + backgroundColorId = R.color.tile_4; textColorId = R.color.text_tile_low; textSizeId = R.dimen.text_size_tile_small; break; case 8: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_8_color)); - textColorId = R.color.text_tile_low; // Devrait être high + backgroundColorId = R.color.tile_8; + textColorId = R.color.text_tile_high; // Correction couleur texte textSizeId = R.dimen.text_size_tile_small; break; case 16: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_16_color)); - textColorId = R.color.text_tile_low; // Devrait être high + backgroundColorId = R.color.tile_16; + textColorId = R.color.text_tile_high; // Correction textSizeId = R.dimen.text_size_tile_small; break; case 32: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_32_color)); - textColorId = R.color.text_tile_low; // Devrait être high + backgroundColorId = R.color.tile_32; + textColorId = R.color.text_tile_high; // Correction textSizeId = R.dimen.text_size_tile_small; break; case 64: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_64_color)); - textColorId = R.color.text_tile_low; // Devrait être high + backgroundColorId = R.color.tile_64; + textColorId = R.color.text_tile_high; // Correction textSizeId = R.dimen.text_size_tile_small; break; case 128: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_128_color)); + backgroundColorId = R.color.tile_128; textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_medium; + textSizeId = R.dimen.text_size_tile_medium; // Changement taille break; case 256: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_256_color)); + backgroundColorId = R.color.tile_256; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break; case 512: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_512_color)); + backgroundColorId = R.color.tile_512; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_medium; break; case 1024: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_1024_color)); + backgroundColorId = R.color.tile_1024; textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_large; + textSizeId = R.dimen.text_size_tile_large; // Changement taille break; case 2048: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_2048_color)); + backgroundColorId = R.color.tile_2048; textColorId = R.color.text_tile_high; textSizeId = R.dimen.text_size_tile_large; break; - default: - tile.setBackgroundResource(R.drawable.tile_background); - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_empty_color)); - textColorId = android.R.color.transparent; - textSizeId = R.dimen.text_size_tile_small; - if (value > 2048) { - tile.getBackground().setTint(ContextCompat.getColor(this, R.color.tile_super_color)); - textColorId = R.color.text_tile_high; - textSizeId = R.dimen.text_size_tile_large; - } + default: // > 2048 + backgroundColorId = R.color.tile_super; + textColorId = R.color.text_tile_high; + textSizeId = R.dimen.text_size_tile_large; break; } - if (value > 0) { - tile.setText(String.valueOf(value)); - tile.setTextColor(ContextCompat.getColor(this, textColorId)); - tile.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); - } else { - tile.setText(""); - } - tile.setGravity(Gravity.CENTER); + tileTextView.setBackgroundResource(R.drawable.tile_background); // Utilise drawable unique + tileTextView.getBackground().setTint(ContextCompat.getColor(this, backgroundColorId)); // Applique teinte + + tileTextView.setTextColor(ContextCompat.getColor(this, textColorId)); + tileTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(textSizeId)); } - private void setupSwipeListener() { - gameBoardLayout.setOnTouchListener(new OnSwipeTouchListener(MainActivity.this, new OnSwipeTouchListener.SwipeListener() { + + /** + * Configure le listener de swipe sur le GridLayout pour gérer les mouvements des tuiles. + */ + @SuppressLint("ClickableViewAccessibility") // Ajout annotation + private void setupSwipeListener() { // Inchangé mais utilise getter + getBoardGridLayout().setOnTouchListener(new OnSwipeTouchListener(this, new OnSwipeTouchListener.SwipeListener() { @Override public void onSwipeTop() { handleSwipe(Direction.UP); } @Override public void onSwipeBottom() { handleSwipe(Direction.DOWN); } @Override public void onSwipeLeft() { handleSwipe(Direction.LEFT); } @@ -205,104 +347,394 @@ public class MainActivity extends AppCompatActivity { })); } - private void handleSwipe(Direction direction) { + /** + * Gère un swipe dans une direction donnée, en appelant la méthode appropriée de la classe Game + * et en mettant à jour l'interface utilisateur. + * + * @param direction La direction du swipe (UP, DOWN, LEFT, RIGHT). + */ + private void handleSwipe(Direction direction) { // Logique modifiée pour stats et win/loss + boolean boardChanged; // Vérifie si le plateau a changé switch (direction) { - case UP: game.pushUp(); break; - case DOWN: game.pushDown(); break; - case LEFT: game.pushLeft(); break; - case RIGHT: game.pushRight(); break; + case UP: boardChanged = getGame().pushUp(); break; + case DOWN: boardChanged = getGame().pushDown(); break; + case LEFT: boardChanged = getGame().pushLeft(); break; + case RIGHT: boardChanged = getGame().pushRight(); break; + default: boardChanged = false; + } + + if (boardChanged) { + currentMoves++; // Stat + totalMoves++; // Stat + // mergesThisGame++; // Fusion est comptée dans Game ou ici? (pas dans ce snippet) + totalMerges++; // Approximation, devrait être compté par fusion réelle + + // Trouve la tuile la plus haute pour les stats + int currentHighest = 0; + for (int row = 0; row < BOARD_SIZE; row++) { + for (int col = 0; col < BOARD_SIZE; col++) { + if (game.getCellValue(row, col) > currentHighest) { + currentHighest = game.getCellValue(row, col); + } + } + } + if (currentHighest > highestTile) { + highestTile = currentHighest; // Met à jour stat highestTile + } + + + getGame().addNewTile(); // Ajoute une nouvelle tuile + updateUI(); // Met à jour l'affichage + + // Vérifie victoire ou défaite + if (getGame().isGameWon()) { + // Gérer la victoire (par exemple, afficher un message) + if(game.getCellValue(0,0) == 2048){ //Condition spécifique du snippet ? semble être un placeholder + numberOfTimesObjectiveReached++; // Stat + // Gérer les temps de victoire (best/worst) + long timeTaken = System.currentTimeMillis() - currentGameStartTimeMs; + if(timeTaken < bestWinningTimeMs){ + bestWinningTimeMs = timeTaken; + } + if(timeTaken > worstWinningTimeMs){ + worstWinningTimeMs = timeTaken; + } + } + showGameWonDialog(); // Affiche dialog victoire + } else if (getGame().isGameOver()) { + // Gérer la défaite + totalGamesPlayed++; // Stat + showGameOverDialog(); // Affiche dialog défaite + } + } - game.addNewNumbers(); - updateUI(); } - // Modifié pour montrer la dialog - private void restartGame() { - showRestartConfirmationDialog(); - } - // Nouvelle méthode pour la dialog - private void showRestartConfirmationDialog() { + /** + * Énumération représentant les directions possibles de swipe. + */ + enum Direction { UP, DOWN, LEFT, RIGHT } + + /** + * Affiche une boîte de dialogue de confirmation avant de recommencer la partie. + * Utilise un layout XML personnalisé (dialog_restart_confirm.xml). + */ + private void showRestartConfirmationDialog() { // Inchangé mais appelle startNewGame AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null); builder.setView(dialogView); - // Références vues dialog - TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle); // Non utilisé mais présent dans snippet - TextView dialogMessage = dialogView.findViewById(R.id.dialogMessage); // Non utilisé mais présent dans snippet + TextView dialogTitle = dialogView.findViewById(R.id.dialogTitle); + TextView dialogMessage = dialogView.findViewById(R.id.dialogMessage); Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton); Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton); final AlertDialog dialog = builder.create(); - cancelButton.setOnClickListener(v -> { - dialog.dismiss(); - }); + cancelButton.setOnClickListener(v -> dialog.dismiss()); confirmButton.setOnClickListener(v -> { dialog.dismiss(); - game = new Game(); // Création nouveau jeu - game.addNewNumbers(); // Ajout tuiles initiales - game.addNewNumbers(); - updateUI(); // Mise à jour UI + startNewGame(); // Appelle la nouvelle méthode }); dialog.show(); } - - private void showStats() { - //A faire - } - private void showMenu() { - //A faire + /** + * Commence une nouvelle partie : réinitialise le jeu et les statistiques de la partie en cours. + */ + private void startNewGame() { // Nouvelle méthode (anciennement restartGame) + reset stats + totalGamesStarted++; // Stat + currentMoves = 0; // Reset stat partie + mergesThisGame = 0; // Reset stat partie + currentGameStartTimeMs = System.currentTimeMillis(); // Reset timer partie + setGame(new Game(this)); // Crée un nouveau jeu + // game.addNewTile(); // Le constructeur de Game s'en charge maintenant + // game.addNewTile(); + updateUI(); // Met à jour l'affichage } - private void showMultiplayerScreen() { - //A faire - } - // Ajout onPause - @Override - protected void onPause() { - super.onPause(); - saveGame(); // Sauvegarde en quittant - } + /** + * Affiche les statistiques du jeu en utilisant un ViewStub pour le chargement différé. + */ + private void showStatistics() { // Nouvelle méthode + statisticsVisible = !statisticsVisible; - // Nouvelle méthode sauvegarde - private void saveGame() { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(GAME_STATE_KEY, game.serialize()); // Sauvegarde état sérialisé - editor.putInt(HIGH_SCORE_KEY, game.getHighScore()); // Sauvegarde high score - editor.apply(); // Appliquer - } - - // Nouvelle méthode chargement - private void loadGame() { - String gameState = sharedPreferences.getString(GAME_STATE_KEY, null); // Récupère état - if (gameState != null) { - game = Game.deserialize(gameState); // Restaure si existe - // game.loadHighScore(); // Pas dans le snippet loadGame + if (statisticsVisible) { + if (statisticsViewStub.getParent() != null) { + statisticsViewStub.inflate(); + } + updateStatisticsTextViews(); // Met à jour les textes des stats + findViewById(R.id.statistics_layout).setVisibility(View.VISIBLE); // Rend visible le layout gonflé + getMultiplayerButton().setVisibility(View.GONE); // Cache bouton multi } else { - game = new Game(); // Nouveau jeu sinon - game.addNewNumbers(); - game.addNewNumbers(); + hideStatistics(); // Cache si on reclique } - game.loadHighScore(); // Présent dans le snippet loadGame - updateUI(); // MAJ UI - - // Logique high score séparée comme dans snippet - int savedHighScore = sharedPreferences.getInt(HIGH_SCORE_KEY, 0); - if (savedHighScore > game.getHighScore()) { - game.setHighScore(savedHighScore); - // updateUI(); // Pas de second updateUI dans le snippet ici - } - } - private enum Direction { - UP, DOWN, LEFT, RIGHT + /** + * Met à jour tous les TextViews à l'intérieur du layout des statistiques (qui doit déjà être gonflé). + */ + private void updateStatisticsTextViews() { // Nouvelle méthode + View inflatedStatsView = findViewById(R.id.statistics_layout); // Récupère la vue gonflée + if (inflatedStatsView != null) { // Vérifie si la vue existe + TextView highScoreStatsLabel = inflatedStatsView.findViewById(R.id.high_score_stats_label); + TextView totalGamesPlayedLabel = inflatedStatsView.findViewById(R.id.total_games_played_label); + TextView totalGamesStartedLabel = inflatedStatsView.findViewById(R.id.total_games_started_label); + TextView winPercentageLabel = inflatedStatsView.findViewById(R.id.win_percentage_label); + TextView totalPlayTimeLabel = inflatedStatsView.findViewById(R.id.total_play_time_label); + TextView totalMovesLabel = inflatedStatsView.findViewById(R.id.total_moves_label); + TextView currentMovesLabel = inflatedStatsView.findViewById(R.id.current_moves_label); + TextView currentGameTimeLabel = inflatedStatsView.findViewById(R.id.current_game_time_label); + TextView averageGameTimeLabel = inflatedStatsView.findViewById(R.id.average_game_time_label); // Pour solo + TextView bestWinningTimeLabel = inflatedStatsView.findViewById(R.id.best_winning_time_label); + TextView worstWinningTimeLabel = inflatedStatsView.findViewById(R.id.worst_winning_time_label); + TextView totalMergesLabel = inflatedStatsView.findViewById(R.id.total_merges_label); + TextView highestTileLabel = inflatedStatsView.findViewById(R.id.highest_tile_label); + TextView objectiveReachedLabel = inflatedStatsView.findViewById(R.id.number_of_time_objective_reached_label); + TextView perfectGameLabel = inflatedStatsView.findViewById(R.id.perfect_game_label); + TextView multiplayerGamesWonLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_won_label); + TextView multiplayerGamesPlayedLabel = inflatedStatsView.findViewById(R.id.multiplayer_games_played_label); + TextView multiplayerWinRateLabel = inflatedStatsView.findViewById(R.id.multiplayer_win_rate_label); + TextView multiplayerBestWinningStreakLabel = inflatedStatsView.findViewById(R.id.multiplayer_best_winning_streak_label); + TextView multiplayerAverageScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_average_score_label); + TextView averageTimePerGameMultiLabel = inflatedStatsView.findViewById(R.id.average_time_per_game_label); // ID dupliqué? Utilisons le 2eme + TextView totalMultiplayerLossesLabel = inflatedStatsView.findViewById(R.id.total_multiplayer_losses_label); + TextView multiplayerHighScoreLabel = inflatedStatsView.findViewById(R.id.multiplayer_high_score_label); + TextView mergesThisGameLabel = inflatedStatsView.findViewById(R.id.merges_this_game); + + // MAJ Textes + highScoreStatsLabel.setText(getString(R.string.high_score_stats, game.getHighestScore())); + totalGamesPlayedLabel.setText(getString(R.string.total_games_played, totalGamesPlayed)); + totalGamesStartedLabel.setText(getString(R.string.total_games_started, totalGamesStarted)); + totalMovesLabel.setText(getString(R.string.total_moves, totalMoves)); + currentMovesLabel.setText(getString(R.string.current_moves, currentMoves)); + mergesThisGameLabel.setText(getString(R.string.merges_this_game_label, mergesThisGame)); + totalMergesLabel.setText(getString(R.string.total_merges, totalMerges)); + highestTileLabel.setText(getString(R.string.highest_tile, highestTile)); + objectiveReachedLabel.setText(getString(R.string.number_of_time_objective_reached, numberOfTimesObjectiveReached)); + perfectGameLabel.setText(getString(R.string.perfect_games, perfectGames)); // Il manque la stat perfectGames dans les champs + multiplayerGamesWonLabel.setText(getString(R.string.multiplayer_games_won, multiplayerGamesWon)); + multiplayerGamesPlayedLabel.setText(getString(R.string.multiplayer_games_played, multiplayerGamesPlayed)); + multiplayerBestWinningStreakLabel.setText(getString(R.string.multiplayer_best_winning_streak, multiplayerBestWinningStreak)); + multiplayerAverageScoreLabel.setText(getString(R.string.multiplayer_average_score, multiplayerGamesPlayed > 0 ? (totalMerges / multiplayerGamesPlayed) : 0)); // Calcul approximatif + totalMultiplayerLossesLabel.setText(getString(R.string.total_multiplayer_losses, totalMultiplayerLosses)); + multiplayerHighScoreLabel.setText(getString(R.string.multiplayer_high_score, multiplayerHighestScore)); + + // Calculs Pourcentages et Temps + String winPercentage = (totalGamesStarted > 0) ? String.format("%.2f%%", ((double) numberOfTimesObjectiveReached / totalGamesStarted) * 100) : "N/A"; // Utilise numberOfTimesObjectiveReached + winPercentageLabel.setText(getString(R.string.win_percentage, winPercentage)); + + String multiplayerWinRate = (multiplayerGamesPlayed > 0) ? String.format("%.2f%%", ((double) multiplayerGamesWon / multiplayerGamesPlayed) * 100) : "N/A"; + multiplayerWinRateLabel.setText(getString(R.string.multiplayer_win_rate, multiplayerWinRate)); + + totalPlayTimeLabel.setText(getString(R.string.total_play_time, formatTime(totalPlayTimeMs))); + // Calcule le temps de la partie en cours dynamiquement si elle n'est pas finie + long currentDuration = (game != null && !game.isGameOver() && !game.isGameWon()) ? System.currentTimeMillis() - currentGameStartTimeMs : 0; + currentGameTimeLabel.setText(getString(R.string.current_game_time, formatTime(currentDuration))); + + long avgTimeSolo = (numberOfTimesObjectiveReached > 0) ? totalPlayTimeMs / numberOfTimesObjectiveReached : 0; // Basé sur victoires? Ou parties jouées? + averageGameTimeLabel.setText(getString(R.string.average_time_per_game, formatTime(avgTimeSolo))); // ID 1 + + long avgTimeMulti = (multiplayerGamesPlayed > 0) ? multiplayerAverageGameTimeMs / multiplayerGamesPlayed : 0; + averageTimePerGameMultiLabel.setText(getString(R.string.average_time_per_game_label, formatTime(avgTimeMulti))); // ID 2 + + bestWinningTimeLabel.setText(getString(R.string.best_winning_time, (bestWinningTimeMs != Long.MAX_VALUE) ? formatTime(bestWinningTimeMs) : "N/A")); + worstWinningTimeLabel.setText(getString(R.string.worst_winning_time, (worstWinningTimeMs != 0) ? formatTime(worstWinningTimeMs) : "N/A")); + + // Listener bouton Back dans les stats + Button backButton = inflatedStatsView.findViewById(R.id.backButton); + backButton.setOnClickListener(v -> hideStatistics()); + } + } + + /** + * Masque la vue des statistiques et réaffiche le bouton multijoueur. + */ + private void hideStatistics() { // Nouvelle méthode + View inflatedStatsView = findViewById(R.id.statistics_layout); + if (inflatedStatsView != null) { + inflatedStatsView.setVisibility(View.GONE); + } + getMultiplayerButton().setVisibility(View.VISIBLE); // Réaffiche bouton multi + statisticsVisible = false; // Met à jour l'état + } + + /** + * Charge les statistiques depuis les SharedPreferences. + */ + private void loadStatistics() { // Nouvelle méthode + totalGamesPlayed = preferences.getInt("totalGamesPlayed", 0); + totalGamesStarted = preferences.getInt("totalGamesStarted", 0); + totalMoves = preferences.getInt("totalMoves", 0); + // currentMoves n'est pas chargé, il est spécifique à la session + totalPlayTimeMs = preferences.getLong("totalPlayTimeMs", 0); + // currentGameStartTimeMs est initialisé dans initializeGame ou repris + // mergesThisGame n'est pas chargé + totalMerges = preferences.getInt("totalMerges", 0); + highestTile = preferences.getInt("highestTile", 0); + numberOfTimesObjectiveReached = preferences.getInt("numberOfTimesObjectiveReached", 0); + perfectGames = preferences.getInt("perfectGames", 0); // Chargement stat + // averageGameTimeMs n'est pas chargé, recalculé + bestWinningTimeMs = preferences.getLong("bestWinningTimeMs", Long.MAX_VALUE); + worstWinningTimeMs = preferences.getLong("worstWinningTimeMs", 0); + + multiplayerGamesWon = preferences.getInt("multiplayerGamesWon", 0); + multiplayerGamesPlayed = preferences.getInt("multiplayerGamesPlayed", 0); + multiplayerBestWinningStreak = preferences.getInt("multiplayerBestWinningStreak", 0); + // multiplayerAverageScore recalculé + multiplayerAverageGameTimeMs = preferences.getLong("multiplayerAverageGameTimeMs", 0); + totalMultiplayerLosses = preferences.getInt("totalMultiplayerLosses", 0); + multiplayerHighestScore = preferences.getInt("multiplayerHighScore", 0); + } + + /** + * Sauvegarde les statistiques dans les SharedPreferences. + */ + private void saveStatistics() { // Nouvelle méthode + SharedPreferences.Editor editor = preferences.edit(); + editor.putInt("totalGamesPlayed", totalGamesPlayed); + editor.putInt("totalGamesStarted", totalGamesStarted); + editor.putInt("totalMoves", totalMoves); + // currentMoves non sauvegardé + editor.putLong("totalPlayTimeMs", totalPlayTimeMs); + // currentGameStartTimeMs non sauvegardé directement, utilisé pour calculer totalPlayTimeMs + // mergesThisGame non sauvegardé + editor.putInt("totalMerges", totalMerges); + editor.putInt("highestTile", highestTile); + editor.putInt("numberOfTimesObjectiveReached", numberOfTimesObjectiveReached); + editor.putInt("perfectGames", perfectGames); // Sauvegarde stat + // averageGameTimeMs non sauvegardé + editor.putLong("bestWinningTimeMs", bestWinningTimeMs); + editor.putLong("worstWinningTimeMs", worstWinningTimeMs); + + editor.putInt("multiplayerGamesWon", multiplayerGamesWon); + editor.putInt("multiplayerGamesPlayed", multiplayerGamesPlayed); + editor.putInt("multiplayerBestWinningStreak", multiplayerBestWinningStreak); + // multiplayerAverageScore non sauvegardé + editor.putLong("multiplayerAverageGameTimeMs", multiplayerAverageGameTimeMs); + editor.putInt("totalMultiplayerLosses", totalMultiplayerLosses); + editor.putInt("multiplayerHighScore", multiplayerHighestScore); + + editor.apply(); + } + + /** + * Formate une durée en millisecondes en une chaîne de caractères (hh:mm:ss ou mm:ss). + */ + private String formatTime(long milliseconds) { // Nouvelle méthode + long seconds = (milliseconds / 1000) % 60; + long minutes = (milliseconds / (1000 * 60)) % 60; + long hours = milliseconds / (1000 * 60 * 60); + + if (hours > 0) { + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%02d:%02d", minutes, seconds); + } + } + + /** + * Affiche l'écran du menu (placeholder avec AlertDialog). + */ + private void showMenu() { // Modifié pour utiliser AlertDialog + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Menu") + .setMessage("Fonctionnalité de menu à venir !") + .setPositiveButton("OK", (dialog, id) -> { /* Rien */ }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + /** + * Affiche l'écran du mode multijoueur (placeholder avec AlertDialog). + */ + private void showMultiplayerScreen() { // Modifié pour utiliser AlertDialog + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Multijoueur") + .setMessage("Fonctionnalité multijoueur à venir !") + .setPositiveButton("OK", (dialog, id) -> { /* Rien */ }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + @Override + protected void onResume() { // Ajouté/Modifié + super.onResume(); + // Ne redémarre le chrono que si la partie n'est pas finie + if (game != null && !game.isGameOver() && !game.isGameWon()) { + currentGameStartTimeMs = System.currentTimeMillis(); + } + // Réaffiche les stats si elles étaient visibles + if (statisticsVisible) { + // Assure-toi que la vue est prête avant de mettre à jour + if (findViewById(R.id.statistics_layout) != null) { + updateStatisticsTextViews(); + findViewById(R.id.statistics_layout).setVisibility(View.VISIBLE); + getMultiplayerButton().setVisibility(View.GONE); + } else { + // Si la vue n'est pas encore gonflée, on la montre (ce qui la gonflera et mettra à jour) + showStatistics(); + } + } + } + + @Override + protected void onPause() { // Modifié pour calculer temps et sauver stats + super.onPause(); + if (game != null) { + // Ajoute le temps écoulé seulement si la partie n'est pas finie + if (!game.isGameOver() && !game.isGameWon()) { + totalPlayTimeMs += System.currentTimeMillis() - currentGameStartTimeMs; + } + saveGame(); // Sauvegarde état jeu + saveStatistics(); // Sauvegarde stats + } + } + + private void saveGame() { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(GAME_STATE_KEY, game.toString()); + editor.putInt(HIGH_SCORE_KEY, game.getHighestScore()); + editor.apply(); + } + + /** + * Affiche une boîte de dialogue lorsque le joueur gagne la partie (atteint 2048). + */ + private void showGameWonDialog() { // Ajouté + // Met à jour les stats de victoire avant d'afficher + numberOfTimesObjectiveReached++; + long timeTaken = System.currentTimeMillis() - currentGameStartTimeMs; + if(timeTaken < bestWinningTimeMs){ bestWinningTimeMs = timeTaken; } + if(timeTaken > worstWinningTimeMs){ worstWinningTimeMs = timeTaken; } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Vous avez gagné !") + .setMessage("Félicitations ! Vous avez atteint 2048 !") + .setPositiveButton("Nouvelle partie", (dialog, which) -> startNewGame()) + .setNegativeButton("Quitter", (dialog, which) -> finish()) + .setCancelable(false) + .show(); + } + + /** + * Affiche une boîte de dialogue lorsque le joueur perd la partie (aucun mouvement possible). + */ + private void showGameOverDialog() { // Ajouté + totalGamesPlayed++; // Met à jour stat avant d'afficher + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Partie terminée !") + .setMessage("Aucun mouvement possible. Votre score final est : " + game.getCurrentScore()) + .setPositiveButton("Nouvelle partie", (dialog, which) -> startNewGame()) + .setNegativeButton("Quitter", (dialog, which) -> finish()) + .setCancelable(false) + .show(); } } \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java index 001f050..caf3a09 100644 --- a/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java +++ b/app/src/main/java/legion/muyue/best2048/OnSwipeTouchListener.java @@ -1,72 +1,152 @@ +// Fichier OnSwipeTouchListener.java +// Classe utilitaire pour détecter les gestes de balayage (swipe). +/* + Fonctions principales : + - Utilise GestureDetector pour détecter les swipes. + - Définit une interface SwipeListener pour notifier la classe appelante (MainActivity). + - onFling() : Détecte la direction du swipe (haut, bas, gauche, droite). + - SWIPE_THRESHOLD, SWIPE_VELOCITY_THRESHOLD : Constantes pour la sensibilité du swipe. + + Relations : + - MainActivity : MainActivity crée une instance et est notifiée des swipes via l'interface SwipeListener. +*/ + package legion.muyue.best2048; +import android.annotation.SuppressLint; // Ajout import android.content.Context; +// import android.location.GnssAntennaInfo; // Import inutile trouvé dans le dump import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; +import androidx.annotation.NonNull; // Ajout + public class OnSwipeTouchListener implements View.OnTouchListener { private final GestureDetector gestureDetector; - private final SwipeListener listener; // Ajout de l'interface listener + private final SwipeListener listener; - // Interface pour les événements de swipe + /** + * Interface pour les événements de swipe. Toute classe qui veut réagir aux swipes + * doit implémenter cette interface et passer une instance à OnSwipeTouchListener. + */ public interface SwipeListener { + /** + * Appelée lorsqu'un swipe vers le haut est détecté. + */ void onSwipeTop(); + + /** + * Appelée lorsqu'un swipe vers le bas est détecté. + */ void onSwipeBottom(); + + /** + * Appelée lorsqu'un swipe vers la gauche est détecté. + */ void onSwipeLeft(); + + /** + * Appelée lorsqu'un swipe vers la droite est détecté. + */ void onSwipeRight(); } - // Constructeur modifié pour accepter le listener + /** + * Constructeur de la classe OnSwipeTouchListener. + * + * @param ctx Le contexte de l'application. Nécessaire pour GestureDetector. + * @param listener L'instance qui écoutera les événements de swipe. + */ public OnSwipeTouchListener(Context ctx, SwipeListener listener) { gestureDetector = new GestureDetector(ctx, new GestureListener()); this.listener = listener; } + /** + * Méthode appelée lorsqu'un événement tactile se produit sur la vue attachée. + * Elle transmet l'événement au GestureDetector pour analyse. + * + * @param v La vue sur laquelle l'événement tactile s'est produit. + * @param event L'objet MotionEvent décrivant l'événement tactile. + * @return true si l'événement a été géré, false sinon. + */ + @SuppressLint("ClickableViewAccessibility") // Ajout @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } + /** + * Classe interne qui étend GestureDetector.SimpleOnGestureListener pour gérer + * spécifiquement les gestes de balayage (fling). + */ private final class GestureListener extends GestureDetector.SimpleOnGestureListener { - private static final int SWIPE_THRESHOLD = 100; - private static final int SWIPE_VELOCITY_THRESHOLD = 100; + private static final int SWIPE_THRESHOLD = 100; // Distance minimale du swipe (en pixels). + private static final int SWIPE_VELOCITY_THRESHOLD = 100; // Vitesse minimale du swipe (en pixels par seconde). + /** + * Méthode appelée lorsqu'un appui initial sur l'écran est détecté. + * On retourne toujours 'true' pour indiquer qu'on gère cet événement. + * + * @param e L'objet MotionEvent décrivant l'appui initial. + * @return true, car on gère toujours l'événement 'onDown'. + */ @Override - public boolean onDown(MotionEvent e) { + public boolean onDown(@NonNull MotionEvent e) { // Ajout @NonNull return true; } + /** + * Méthode appelée lorsqu'un geste de balayage (fling) est détecté. + * + * @param e1 L'objet MotionEvent du premier appui (début du swipe). Peut être null. + * @param e2 L'objet MotionEvent de la fin du swipe. + * @param velocityX La vitesse du swipe en pixels par seconde sur l'axe X. + * @param velocityY La vitesse du swipe en pixels par seconde sur l'axe Y. + * @return true si le geste de balayage a été géré, false sinon. + */ @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { // Ajout @NonNull + // Vérification de nullité pour e1, nécessaire car onFling peut être appelé même si onDown retourne false ou si le geste est complexe. + if (e1 == null) { + return false; + } + boolean result = false; try { - float diffY = e2.getY() - e1.getY(); - float diffX = e2.getX() - e1.getX(); + float diffY = e2.getY() - e1.getY(); // Différence de position sur l'axe Y. + float diffX = e2.getX() - e1.getX(); // Différence de position sur l'axe X. + + // Détermine si le swipe est plutôt horizontal ou vertical. if (Math.abs(diffX) > Math.abs(diffY)) { + // Swipe horizontal. if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { if (diffX > 0) { - listener.onSwipeRight(); // Appel via listener + listener.onSwipeRight(); // Swipe vers la droite. } else { - listener.onSwipeLeft(); // Appel via listener + listener.onSwipeLeft(); // Swipe vers la gauche. } result = true; } - } else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { - if (diffY > 0) { - listener.onSwipeBottom(); // Appel via listener - } else { - listener.onSwipeTop(); // Appel via listener + } else { + // Swipe vertical. + if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY > 0) { + listener.onSwipeBottom(); // Swipe vers le bas. + } else { + listener.onSwipeTop(); // Swipe vers le haut. + } + result = true; } - result = true; } } catch (Exception exception) { - exception.printStackTrace(); + // exception.printStackTrace(); // Commenté dans le nouveau code ? Gardons commenté. + exception.fillInStackTrace(); // Gestion des erreurs (journalisation). } return result; } } - // Les méthodes locales vides ne sont plus nécessaires car on utilise l'interface } \ No newline at end of file diff --git a/app/src/main/res/anim/button_press.xml b/app/src/main/res/anim/button_press.xml index 2c62956..1712660 100644 --- a/app/src/main/res/anim/button_press.xml +++ b/app/src/main/res/anim/button_press.xml @@ -1,15 +1,18 @@ + + + android:toXScale="0.95" + android:toYScale="0.95" /> + + android:toAlpha="0.8" /> \ No newline at end of file diff --git a/app/src/main/res/anim/button_release.xml b/app/src/main/res/anim/button_release.xml index f796d29..bc096ab 100644 --- a/app/src/main/res/anim/button_release.xml +++ b/app/src/main/res/anim/button_release.xml @@ -1,15 +1,18 @@ + + + android:toXScale="1.0" + android:toYScale="1.0" /> + + android:toAlpha="1.0" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/button_multiplayer_background.xml b/app/src/main/res/drawable/button_multiplayer_background.xml index 20d0310..30392a1 100644 --- a/app/src/main/res/drawable/button_multiplayer_background.xml +++ b/app/src/main/res/drawable/button_multiplayer_background.xml @@ -1,6 +1,35 @@ - - - - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml index 66a2b84..4a5537c 100644 --- a/app/src/main/res/drawable/dialog_background.xml +++ b/app/src/main/res/drawable/dialog_background.xml @@ -2,7 +2,5 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/drawable/tile_background.xml b/app/src/main/res/drawable/tile_background.xml index c1763d7..a572fe5 100644 --- a/app/src/main/res/drawable/tile_background.xml +++ b/app/src/main/res/drawable/tile_background.xml @@ -1,6 +1,12 @@ - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index caef3b4..a4f7898 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,23 +1,21 @@ - @@ -57,13 +55,14 @@ + android:text="@string/score_placeholder" /> + android:text="@string/high_score_placeholder" /> + @@ -96,8 +95,7 @@