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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_game_won.xml b/app/src/main/res/layout/dialog_game_won.xml
new file mode 100644
index 0000000..6ed10e7
--- /dev/null
+++ b/app/src/main/res/layout/dialog_game_won.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c0294aa..8a2935f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,4 +44,11 @@
Single PlayerMultiplayerBack
+ You won!
+ Congratulations, you\'ve reached 2048!
+ Continue
+ New Part
+ Game over!
+ No move possible.\nFinal score: %d
+ To leave
\ No newline at end of file