diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index b22abb3..363041f 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -26,6 +26,7 @@ package legion.muyue.best2048; import android.annotation.SuppressLint; import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.util.TypedValue; @@ -61,14 +62,17 @@ public class MainActivity extends AppCompatActivity { private GameStats gameStats; // Instance pour gérer les stats private static final int BOARD_SIZE = 4; + // --- State Management --- + private boolean statisticsVisible = false; + private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } // Nouvel état de jeu + private GameFlowState currentGameState = GameFlowState.PLAYING; // Initialisation + // --- Preferences --- private SharedPreferences preferences; private static final String PREFS_NAME = "Best2048_Prefs"; private static final String HIGH_SCORE_KEY = "high_score"; private static final String GAME_STATE_KEY = "game_state"; - private boolean statisticsVisible = false; - // --- Activity Lifecycle --- @Override @@ -139,16 +143,22 @@ public class MainActivity extends AppCompatActivity { */ private void initializeGameAndStats() { preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - gameStats = new GameStats(this); // Crée et charge les stats (y.c. overallHighScore) - loadGame(); // Charge l'état du jeu, crée un nouveau si nécessaire, synchronise le HS - updateUI(); // Affiche l'état chargé ou nouveau - - // Si loadGame a résulté en une nouvelle partie (game==null avant ou deserialize a échoué), - // on s'assure que les stats de la partie en cours sont bien initialisées. - if (game == null || preferences.getString(GAME_STATE_KEY, null) == null) { - startNewGame(); // Assure un état cohérent si aucun jeu n'a été chargé + gameStats = new GameStats(this); + loadGame(); // Charge jeu et met à jour high score + updateUI(); + if (game == null) { + startNewGame(); // Assure une partie valide si chargement échoue } else { - // Le timer de la partie chargée sera (re)démarré dans onResume + // Détermine l'état initial basé sur le jeu chargé + if (game.isGameOver()) { + currentGameState = GameFlowState.GAME_OVER; + } else if (game.isGameWon()) { + // Si on charge une partie déjà gagnée, on considère qu'on a déjà vu la dialog + currentGameState = GameFlowState.WON_DIALOG_SHOWN; + } else { + currentGameState = GameFlowState.PLAYING; + // Redémarre le timer dans onResume + } } } @@ -266,72 +276,119 @@ public class MainActivity extends AppCompatActivity { } /** - * Traite un geste de swipe : appelle la logique de jeu, met à jour les statistiques, - * ajoute une nouvelle tuile, met à jour l'UI et vérifie les conditions de fin de partie. - * @param direction Direction du swipe. + * Traite un geste de swipe de l'utilisateur sur le plateau de jeu. + * 1. Tente d'effectuer le mouvement dans l'objet Game. + * 2. Si le mouvement a modifié le plateau (boardChanged == true) : + * - Met à jour les statistiques (mouvements, fusions, score, etc.). + * - Ajoute une nouvelle tuile aléatoire. + * - Met à jour l'affichage (UI). + * 3. Vérifie l'état du jeu (gagné ou perdu) APRÈS la tentative de mouvement, + * que le plateau ait changé ou non. C'est la correction clé. + * 4. Affiche les boîtes de dialogue appropriées (victoire, défaite). + * + * @param direction La direction du swipe détecté (UP, DOWN, LEFT, RIGHT). */ private void handleSwipe(Direction direction) { - if (game == null || gameStats == null || game.isGameOver() || game.isGameWon()) return; - - int scoreBefore = game.getCurrentScore(); - boolean boardChanged; - switch (direction) { /* ... appelle game.pushX() ... */ - case UP: boardChanged = game.pushUp(); break; - case DOWN: boardChanged = game.pushDown(); break; - case LEFT: boardChanged = game.pushLeft(); break; - case RIGHT: boardChanged = game.pushRight(); break; - default: boardChanged = false; + // Si le jeu n'est pas initialisé ou s'il est DÉJÀ terminé, ignorer le swipe. + if (game == null || gameStats == null || currentGameState == GameFlowState.GAME_OVER) { + return; } + // Stocker le score avant le mouvement pour calculer le delta + int scoreBefore = game.getCurrentScore(); + // Indique si le mouvement a effectivement changé l'état du plateau + boolean boardChanged = false; + + // --- 1. Tenter d'effectuer le mouvement --- + // Les méthodes pushX() de l'objet Game contiennent la logique de déplacement/fusion + // et appellent en interne checkWinCondition() et checkGameOverCondition() + // pour mettre à jour les états isGameWon() et isGameOver(). + switch (direction) { + case UP: + boardChanged = game.pushUp(); + break; + case DOWN: + boardChanged = game.pushDown(); + break; + case LEFT: + boardChanged = game.pushLeft(); + break; + case RIGHT: + boardChanged = game.pushRight(); + break; + } + + // --- 2. Traiter les conséquences SI le plateau a changé --- if (boardChanged) { - gameStats.recordMove(); // Met à jour stats mouvement + // Mettre à jour les statistiques liées au mouvement réussi + gameStats.recordMove(); int scoreAfter = game.getCurrentScore(); int scoreDelta = scoreAfter - scoreBefore; if (scoreDelta > 0) { - gameStats.recordMerge(1); // Met à jour stats fusion (simplifié) - // Met à jour le highScore dans Game et GameStats si nécessaire + // Supposition simpliste : une augmentation de score implique au moins une fusion + gameStats.recordMerge(1); + // Vérifier et mettre à jour le meilleur score si nécessaire if (scoreAfter > game.getHighestScore()) { - game.setHighestScore(scoreAfter); - gameStats.setHighestScore(scoreAfter); // Synchronise avec GameStats + game.setHighestScore(scoreAfter); // Met à jour dans l'objet Game + gameStats.setHighestScore(scoreAfter); // Met à jour et sauvegarde dans GameStats } } + // Mettre à jour la tuile la plus haute atteinte + gameStats.updateHighestTile(game.getHighestTileValue()); - gameStats.updateHighestTile(game.getHighestTileValue()); // Met à jour stat tuile max + // Ajouter une nouvelle tuile aléatoire sur le plateau + game.addNewTile(); - game.addNewTile(); // Ajoute tuile - updateUI(); // Met à jour affichage + // Mettre à jour l'affichage complet du plateau et des scores + updateUI(); - // Vérifie victoire/défaite après MAJ UI - if (game.isGameWon()) { + } + + // --- 3. Vérifier l'état final du jeu (Gagné / Perdu) --- + // Cette vérification est faite APRÈS la tentative de mouvement, + // On vérifie aussi qu'on n'a pas DÉJÀ traité la fin de partie dans ce même appel. + if (currentGameState != GameFlowState.GAME_OVER) { + + // a) Condition de Victoire (atteindre 2048 ou plus) + // On vérifie aussi qu'on était en train de jouer normalement (pas déjà gagné et décidé de continuer) + if (game.isGameWon() && currentGameState == GameFlowState.PLAYING) { + currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Mettre à jour l'état de flux + // Enregistrer les statistiques de victoire long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); gameStats.recordWin(timeTaken); - showGameWonDialog(); + // Afficher la boîte de dialogue de victoire + showGameWonKeepPlayingDialog(); + + // b) Condition de Défaite (Game Over - pas de case vide ET pas de fusion possible) + // Cette condition est vérifiée seulement si on n'a pas déjà gagné. } else if (game.isGameOver()) { + currentGameState = GameFlowState.GAME_OVER; // Mettre à jour l'état de flux + // Enregistrer les statistiques de défaite et finaliser la partie long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs(); - gameStats.recordLoss(); // Appelle endGame implicitement + gameStats.recordLoss(); + gameStats.endGame(timeTaken); // Finalise le temps, etc. + // Afficher la boîte de dialogue de Game Over showGameOverDialog(); + + if (!boardChanged) { + updateUI(); // Assure que le score final affiché est correct. + } } + // c) Ni gagné, ni perdu : Le jeu continue. L'état reste PLAYING ou WON_DIALOG_SHOWN. } } - // Enum Direction (inchangé) enum Direction { UP, DOWN, LEFT, RIGHT } /** * Affiche la boîte de dialogue demandant confirmation avant de redémarrer. */ private void showRestartConfirmationDialog() { - // ... (code de la dialog inchangé, 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); - Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton); - Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton); - final AlertDialog dialog = builder.create(); - cancelButton.setOnClickListener(v -> dialog.dismiss()); - confirmButton.setOnClickListener(v -> { dialog.dismiss(); startNewGame(); }); - dialog.show(); + LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null); + builder.setView(dialogView); Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton); Button confirmButton = dialogView.findViewById(R.id.dialogConfirmButton); + final AlertDialog dialog = builder.create(); cancelButton.setOnClickListener(v -> dialog.dismiss()); + confirmButton.setOnClickListener(v -> { dialog.dismiss(); startNewGame(); }); dialog.show(); } /** @@ -339,11 +396,75 @@ public class MainActivity extends AppCompatActivity { * crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI. */ private void startNewGame() { - gameStats.startGame(); // Réinitialise stats partie en cours (mouvements, temps, etc.) - game = new Game(); // Crée un nouveau jeu logique - // Applique le meilleur score global connu (chargé par gameStats) au nouvel objet Game - game.setHighestScore(gameStats.getOverallHighScore()); - updateUI(); // Rafraîchit l'affichage + gameStats.startGame(); // Réinitialise stats de partie + game = new Game(); // Crée un nouveau jeu + game.setHighestScore(gameStats.getOverallHighScore()); // Applique HS global + currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER + updateUI(); // Met à jour affichage + } + + /** + * Affiche la boîte de dialogue quand 2048 est atteint, en utilisant un layout personnalisé. + * Propose de continuer à jouer ou de commencer une nouvelle partie. + */ + private void showGameWonKeepPlayingDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.dialog_game_won, null); // Gonfle le layout personnalisé + builder.setView(dialogView); + builder.setCancelable(false); // Empêche de fermer sans choisir + + // Récupère les boutons DANS la vue gonflée + Button keepPlayingButton = dialogView.findViewById(R.id.dialogKeepPlayingButton); + Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonWon); + + final AlertDialog dialog = builder.create(); + + keepPlayingButton.setOnClickListener(v -> { + // L'état est déjà WON_DIALOG_SHOWN, on ne fait rien de spécial, le jeu continue. + dialog.dismiss(); + }); + + newGameButton.setOnClickListener(v -> { + dialog.dismiss(); + startNewGame(); // Démarre une nouvelle partie + }); + + dialog.show(); + } + + /** + * Affiche la boîte de dialogue de fin de partie (plus de mouvements), en utilisant un layout personnalisé. + * Propose Nouvelle Partie ou Quitter. + */ + private void showGameOverDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.dialog_game_over, null); // Gonfle le layout personnalisé + builder.setView(dialogView); + builder.setCancelable(false); // Empêche de fermer sans choisir + + // Récupère les vues DANS la vue gonflée + TextView messageTextView = dialogView.findViewById(R.id.dialogMessageGameOver); + Button newGameButton = dialogView.findViewById(R.id.dialogNewGameButtonGameOver); + Button quitButton = dialogView.findViewById(R.id.dialogQuitButtonGameOver); + + // Met à jour le message avec le score final + messageTextView.setText(getString(R.string.game_over_message, game.getCurrentScore())); + + final AlertDialog dialog = builder.create(); + + newGameButton.setOnClickListener(v -> { + dialog.dismiss(); + startNewGame(); // Démarre une nouvelle partie + }); + + quitButton.setOnClickListener(v -> { + dialog.dismiss(); + finish(); // Ferme l'application + }); + + dialog.show(); } @@ -456,28 +577,6 @@ public class MainActivity extends AppCompatActivity { builder.create().show(); } - /** Affiche le dialogue de victoire. */ - private void showGameWonDialog() { /* ... (inchangé) ... */ - 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 le dialogue de défaite. */ - private void showGameOverDialog() { /* ... (inchangé) ... */ - 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(); - } - // --- Sauvegarde / Chargement --- /** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */ @@ -494,27 +593,29 @@ public class MainActivity extends AppCompatActivity { /** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */ private void loadGame() { - String gameState = preferences.getString(GAME_STATE_KEY, null); - int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, gameStats.getOverallHighScore()); // Utilise HS de GameStats comme défaut + String gameStateString = preferences.getString(GAME_STATE_KEY, null); + int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0); // HS lu depuis prefs - // S'assure que GameStats a la dernière version connue du HS - gameStats.setHighestScore(savedHighScore); + if (gameStats != null) { // S'assure que GameStats a le HS correct + gameStats.setHighestScore(savedHighScore); + } - if (gameState != null) { - game = Game.deserialize(gameState); // Désérialise plateau + score + if (gameStateString != null) { + game = Game.deserialize(gameStateString); if (game != null) { - game.setHighestScore(savedHighScore); // Applique le HS à l'objet Game - } else { - // Échec -> Nouvelle partie - game = new Game(); - game.setHighestScore(savedHighScore); - gameStats.startGame(); // Réinitialise stats de partie - } - } else { - // Pas de sauvegarde -> Nouvelle partie + game.setHighestScore(savedHighScore); // Applique HS à Game + // Détermine l'état basé sur le jeu chargé + if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER; + else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Si gagné avant, on continue + else currentGameState = GameFlowState.PLAYING; + } else { game = null; } // Échec désérialisation + } else { game = null; } // Pas de sauvegarde + + if (game == null) { // Si pas de jeu chargé ou erreur game = new Game(); game.setHighestScore(savedHighScore); - gameStats.startGame(); // Réinitialise stats de partie + currentGameState = GameFlowState.PLAYING; + // Pas besoin d'appeler gameStats.startGame() ici, sera fait dans initializeGame OU startNewGame si nécessaire } } diff --git a/app/src/main/res/layout/dialog_game_over.xml b/app/src/main/res/layout/dialog_game_over.xml new file mode 100644 index 0000000..0f03d21 --- /dev/null +++ b/app/src/main/res/layout/dialog_game_over.xml @@ -0,0 +1,53 @@ + + + + + + + + + +