Fix: Amélioration gestion états Victoire et Game Over

- Ajout d'un enum GameFlowState (PLAYING, WON_DIALOG_SHOWN, GAME_OVER) dans MainActivity.
- handleSwipe n'accepte les mouvements que si l'état est PLAYING.
- Lors de la première victoire (>= 2048), affiche une nouvelle boîte de dialogue
  proposant 'Continuer' ou 'Nouvelle Partie'.
  - Si 'Continuer', l'état passe à WON_DIALOG_SHOWN, permettant au jeu de continuer
    sans réafficher la dialogue de victoire.
- En cas de Game Over, l'état passe à GAME_OVER et le jeu est bloqué.
- startNewGame réinitialise l'état à PLAYING.
- loadGame détermine l'état initial basé sur le jeu chargé.
- Mise à jour des strings pour les nouvelles dialogues.
This commit is contained in:
Augustin ROUX 2025-04-04 13:50:24 +02:00
parent 9d8d2c5c62
commit b32d1e0986
4 changed files with 306 additions and 90 deletions

View File

@ -26,6 +26,7 @@ package legion.muyue.best2048;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue; import android.util.TypedValue;
@ -61,14 +62,17 @@ public class MainActivity extends AppCompatActivity {
private GameStats gameStats; // Instance pour gérer les stats private GameStats gameStats; // Instance pour gérer les stats
private static final int BOARD_SIZE = 4; private static final int BOARD_SIZE = 4;
// --- State Management ---
private boolean statisticsVisible = false;
private enum GameFlowState { PLAYING, WON_DIALOG_SHOWN, GAME_OVER } // Nouvel état de jeu
private GameFlowState currentGameState = GameFlowState.PLAYING; // Initialisation
// --- Preferences --- // --- Preferences ---
private SharedPreferences preferences; private SharedPreferences preferences;
private static final String PREFS_NAME = "Best2048_Prefs"; private static final String PREFS_NAME = "Best2048_Prefs";
private static final String HIGH_SCORE_KEY = "high_score"; private static final String HIGH_SCORE_KEY = "high_score";
private static final String GAME_STATE_KEY = "game_state"; private static final String GAME_STATE_KEY = "game_state";
private boolean statisticsVisible = false;
// --- Activity Lifecycle --- // --- Activity Lifecycle ---
@Override @Override
@ -139,16 +143,22 @@ public class MainActivity extends AppCompatActivity {
*/ */
private void initializeGameAndStats() { private void initializeGameAndStats() {
preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
gameStats = new GameStats(this); // Crée et charge les stats (y.c. overallHighScore) gameStats = new GameStats(this);
loadGame(); // Charge l'état du jeu, crée un nouveau si nécessaire, synchronise le HS loadGame(); // Charge jeu et met à jour high score
updateUI(); // Affiche l'état chargé ou nouveau updateUI();
if (game == null) {
// Si loadGame a résulté en une nouvelle partie (game==null avant ou deserialize a échoué), startNewGame(); // Assure une partie valide si chargement échoue
// 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é
} else { } 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, * Traite un geste de swipe de l'utilisateur sur le plateau de jeu.
* ajoute une nouvelle tuile, met à jour l'UI et vérifie les conditions de fin de partie. * 1. Tente d'effectuer le mouvement dans l'objet Game.
* @param direction Direction du swipe. * 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) { private void handleSwipe(Direction direction) {
if (game == null || gameStats == null || game.isGameOver() || game.isGameWon()) return; // 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) {
int scoreBefore = game.getCurrentScore(); return;
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;
} }
// 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) { 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 scoreAfter = game.getCurrentScore();
int scoreDelta = scoreAfter - scoreBefore; int scoreDelta = scoreAfter - scoreBefore;
if (scoreDelta > 0) { if (scoreDelta > 0) {
gameStats.recordMerge(1); // Met à jour stats fusion (simplifié) // Supposition simpliste : une augmentation de score implique au moins une fusion
// Met à jour le highScore dans Game et GameStats si nécessaire gameStats.recordMerge(1);
// Vérifier et mettre à jour le meilleur score si nécessaire
if (scoreAfter > game.getHighestScore()) { if (scoreAfter > game.getHighestScore()) {
game.setHighestScore(scoreAfter); game.setHighestScore(scoreAfter); // Met à jour dans l'objet Game
gameStats.setHighestScore(scoreAfter); // Synchronise avec GameStats 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 // Mettre à jour l'affichage complet du plateau et des scores
updateUI(); // Met à jour affichage 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(); long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordWin(timeTaken); 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()) { } 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(); 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(); 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 } enum Direction { UP, DOWN, LEFT, RIGHT }
/** /**
* Affiche la boîte de dialogue demandant confirmation avant de redémarrer. * Affiche la boîte de dialogue demandant confirmation avant de redémarrer.
*/ */
private void showRestartConfirmationDialog() { private void showRestartConfirmationDialog() {
// ... (code de la dialog inchangé, appelle startNewGame) ...
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater(); LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_restart_confirm, null);
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);
builder.setView(dialogView); final AlertDialog dialog = builder.create(); cancelButton.setOnClickListener(v -> dialog.dismiss());
Button cancelButton = dialogView.findViewById(R.id.dialogCancelButton); confirmButton.setOnClickListener(v -> { dialog.dismiss(); startNewGame(); }); dialog.show();
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. * crée un nouvel objet Game, synchronise le meilleur score et met à jour l'UI.
*/ */
private void startNewGame() { private void startNewGame() {
gameStats.startGame(); // Réinitialise stats partie en cours (mouvements, temps, etc.) gameStats.startGame(); // Réinitialise stats de partie
game = new Game(); // Crée un nouveau jeu logique game = new Game(); // Crée un nouveau jeu
// Applique le meilleur score global connu (chargé par gameStats) au nouvel objet Game game.setHighestScore(gameStats.getOverallHighScore()); // Applique HS global
game.setHighestScore(gameStats.getOverallHighScore()); currentGameState = GameFlowState.PLAYING; // Définit l'état à JOUER
updateUI(); // Rafraîchit l'affichage 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(); 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 / Chargement ---
/** Sauvegarde l'état du jeu et le meilleur score via SharedPreferences. */ /** 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. */ /** Charge l'état du jeu depuis SharedPreferences et synchronise le meilleur score. */
private void loadGame() { private void loadGame() {
String gameState = preferences.getString(GAME_STATE_KEY, null); String gameStateString = preferences.getString(GAME_STATE_KEY, null);
int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, gameStats.getOverallHighScore()); // Utilise HS de GameStats comme défaut int savedHighScore = preferences.getInt(HIGH_SCORE_KEY, 0); // HS lu depuis prefs
// S'assure que GameStats a la dernière version connue du HS if (gameStats != null) { // S'assure que GameStats a le HS correct
gameStats.setHighestScore(savedHighScore); gameStats.setHighestScore(savedHighScore);
}
if (gameState != null) { if (gameStateString != null) {
game = Game.deserialize(gameState); // Désérialise plateau + score game = Game.deserialize(gameStateString);
if (game != null) { if (game != null) {
game.setHighestScore(savedHighScore); // Applique le HS à l'objet Game game.setHighestScore(savedHighScore); // Applique HS à Game
} else { // Détermine l'état basé sur le jeu chargé
// Échec -> Nouvelle partie if (game.isGameOver()) currentGameState = GameFlowState.GAME_OVER;
game = new Game(); else if (game.isGameWon()) currentGameState = GameFlowState.WON_DIALOG_SHOWN; // Si gagné avant, on continue
game.setHighestScore(savedHighScore); else currentGameState = GameFlowState.PLAYING;
gameStats.startGame(); // Réinitialise stats de partie } else { game = null; } // Échec désérialisation
} } else { game = null; } // Pas de sauvegarde
} else {
// Pas de sauvegarde -> Nouvelle partie if (game == null) { // Si pas de jeu chargé ou erreur
game = new Game(); game = new Game();
game.setHighestScore(savedHighScore); 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
} }
} }

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/dialog_background">
<TextView
android:id="@+id/dialogTitleGameOver"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/game_over_title"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_tile_low"
android:gravity="center"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/dialogMessageGameOver" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/game_over_message" android:textSize="16sp"
android:textColor="@color/text_tile_low"
android:gravity="center"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/dialogQuitButtonGameOver"
style="@style/ButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="@string/quit" />
<Button
android:id="@+id/dialogNewGameButtonGameOver"
style="@style/ButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="@string/new_game" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/dialog_background">
<TextView
android:id="@+id/dialogTitleWon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/you_won_title"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_tile_low"
android:gravity="center"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/dialogMessageWon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/you_won_message"
android:textSize="16sp"
android:textColor="@color/text_tile_low"
android:gravity="center"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/dialogNewGameButtonWon"
style="@style/ButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="@string/new_game" />
<Button
android:id="@+id/dialogKeepPlayingButton"
style="@style/ButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="@string/keep_playing" />
</LinearLayout>
</LinearLayout>

View File

@ -44,4 +44,11 @@
<string name="single_player_section">Single Player</string> <string name="single_player_section">Single Player</string>
<string name="multiplayer_section">Multiplayer</string> <string name="multiplayer_section">Multiplayer</string>
<string name="back_button_label">Back</string> <string name="back_button_label">Back</string>
<string name="you_won_title">You won!</string>
<string name="you_won_message">Congratulations, you\'ve reached 2048!</string>
<string name="keep_playing">Continue</string>
<string name="new_game">New Part</string>
<string name="game_over_title">Game over!</string>
<string name="game_over_message">No move possible.\nFinal score: %d</string>
<string name="quit">To leave</string>
</resources> </resources>