Files
Application/README_ARCHITECTURE.md

14 KiB

BoideloV3 - Documentation Architecture

Vue d'ensemble

Ce document décrit l'architecture mise en place pour l'application BoideloV3, un jeu de défi à boire pour Android.

L'architecture suit le pattern Repository + Separation of Concerns, permettant :

  • Une meilleure séparation des responsabilités
  • Un code testable unitairement
  • Une gestion d'erreurs robuste
  • Une maintenance facilitée

Structure du projet

app/src/main/java/com/example/boidelov3/
├── data/                           # Couche d'accès aux données
│   ├── Result.java                 # Wrapper de résultat (succès/erreur)
│   └── QuestionRepository.java     # Repository pour les questions
├── game/                           # Logique métier du jeu
│   └── GameEngine.java             # Moteur de jeu
├── ui/                             # Activités et Fragments
│   ├── Jeux.java                   # Activité principale du jeu
│   ├── JeuxParametres.java         # Écran de paramètres
│   ├── EndGameActivity.java        # Écran de fin de partie
│   └── MainActivity.java           # Écran d'accueil
└── utils/                          # Utilitaires
    ├── DatabaseConnection.java     # Connexion BDD PostgreSQL
    ├── SoundManager.java           # Gestion des sons
    └── BoideloAnimationUtils.java  # Animations UI

app/src/test/java/com/example/boidelov3/
├── data/
│   └── ResultTest.java             # Tests de Result
└── game/
    └── GameEngineTest.java         # Tests de GameEngine

1. Couche Data (données)

1.1 Result<T, E>

Wrapper type-safe pour représenter le résultat d'une opération qui peut échouer.

Avantages :

  • Remplace les exceptions par des valeurs de retour
  • Force le traitement des erreurs
  • Rend le code plus prévisible

Utilisation :

// Créer un résultat succès
Result<String, Exception> success = Result.success("Données chargées");

// Créer un résultat erreur
Result<String, Exception> failure = Result.failure(new Exception("Erreur de chargement"));

// Utiliser le résultat
if (result.isSuccess()) {
    String data = result.getData();
    // Traiter les données
} else {
    Exception error = result.getError();
    // Gérer l'erreur
}

// Ou avec getOrElse
String value = result.getOrElse("valeur par défaut");

Méthodes disponibles :

Méthode Description
success(T data) Crée un résultat réussi
failure(E error) Crée un résultat d'erreur
isSuccess() Vrai si réussi
isFailure() Vrai si échoué
getData() Retourne les données (null si erreur)
getError() Retourne l'erreur (null si succès)
getOrNull() Retourne les données ou null
getOrElse(T default) Retourne les données ou une valeur par défaut

1.2 QuestionRepository

Gère l'accès aux données des questions (JSON + SharedPreferences).

Responsabilités :

  • Charger les questions depuis assets/question.json
  • Suivre les questions déjà posées
  • Sauvegarder les statistiques de jeu

Utilisation :

// Créer le repository
QuestionRepository repository = new QuestionRepository(context);

// Charger les questions
Result<Questions, QuestionLoadException> result = repository.loadQuestions();

if (result.isSuccess()) {
    Questions questions = result.getData();
    List<Question> questionList = questions.getQuestions();
    // Utiliser les questions
} else {
    QuestionLoadException error = result.getError();
    Log.e("TAG", "Erreur: " + error.getMessage());
}

// Obtenir une question aléatoire non posée
Result<Question, QuestionLoadException> randomResult =
    repository.getRandomUnaskedQuestion(questionList);

// Réinitialiser les questions posées
repository.resetAskedQuestions();

// Sauvegarder les stats
repository.saveGameStats(questionsPlayed, playersCount);

Méthodes principales :

Méthode Description
loadQuestions() Charge toutes les questions depuis le JSON
getRandomUnaskedQuestion(List) Retourne une question non posée
markQuestionAsAsked(int) Marque une question comme posée
resetAskedQuestions() Réinitialise le suivi des questions
saveGameStats(int, int) Sauvegarde les statistiques

2. Couche Game (logique métier)

2.1 GameEngine

Contient toute la logique du jeu, indépendante de l'UI.

Responsabilités :

  • Remplacement des variables dans les questions (<J1>, <variante>, etc.)
  • Gestion des manches actives
  • Sélection aléatoire de joueurs
  • Calcul des gorgées

Utilisation :

// Créer le moteur de jeu
GameEngine engine = new GameEngine();

// Préparer les joueurs
List<String> players = Arrays.asList("Alice", "Bob", "Charlie", "David");
int addedGorgees = 2; // Paramètre de jeu

// Traiter une question
Question rawQuestion = ...; // Question depuis le JSON
ProcessedQuestion processed = engine.processQuestion(rawQuestion, players, addedGorgees);

// Afficher la question traitée
textView.setText(Html.fromHtml(processed.question.getQuestion()));

// Vérifier si c'est une manche
if (processed.isManche) {
    // C'est un défi à manches
    Log.d("Game", "Manche active: " + processed.question.getManchesRestantes());
}

// Mettre à jour les manches (à chaque tour)
MancheState mancheState = engine.updateManches();

if (mancheState.endMessage != null) {
    // Une manche s'est terminée
    Toast.makeText(context, mancheState.endMessage, Toast.LENGTH_LONG).show();
} else if (mancheState.activeManche != null) {
    // Une manche est toujours active
    Question activeManche = mancheState.activeManche;
    int remaining = activeManche.getManchesRestantes();
    // Afficher le compteur de manches
}

// Sélectionner des joueurs aléatoires
List<String> selectedPlayers = engine.selectRandomPlayers(players, 3);
// Retourne 3 joueurs uniques

// Vider toutes les manches (fin de partie)
engine.clearManches();

Classes internes :

Classe Description
ProcessedQuestion Conteneur pour une question traitée
MancheState État des manches après mise à jour

Méthodes principales :

Méthode Description
processQuestion(Question, List, int) Traite une question (remplace variables)
selectRandomPlayers(List, int) Sélectionne n joueurs uniques
updateManches() Met à jour et retourne l'état des manches
hasActiveManche() Vrai si une manche est active
clearManches() Vide toutes les manches actives

3. Utilisation recommandée dans une Activity

Voici comment intégrer les nouvelles classes dans Jeux.java :

public class Jeux extends AppCompatActivity {
    private QuestionRepository repository;
    private GameEngine gameEngine;
    private List<String> players;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jeux);

        // Initialiser les composants
        repository = new QuestionRepository(this);
        gameEngine = new GameEngine();
        players = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR");

        // Charger les questions
        loadAndStartGame();
    }

    private void loadAndStartGame() {
        Result<Questions, QuestionRepository.QuestionLoadException> result =
            repository.loadQuestions();

        if (result.isSuccess()) {
            Questions questions = result.getData();
            displayNextQuestion(questions);
        } else {
            Toast.makeText(this, "Erreur de chargement", Toast.LENGTH_SHORT).show();
            finish();
        }
    }

    private void displayNextQuestion(Questions allQuestions) {
        // Obtenir une question aléatoire
        Result<Question, QuestionRepository.QuestionLoadException> result =
            repository.getRandomUnaskedQuestion(allQuestions.getQuestions());

        if (result.isFailure()) {
            endGame();
            return;
        }

        Question question = result.getData();

        // Traiter la question avec le moteur de jeu
        int addedGorgees = getIntent().getIntExtra("EXTRA_AJOUT_GORGEE", 0);
        GameEngine.ProcessedQuestion processed =
            gameEngine.processQuestion(question, players, addedGorgees);

        // Afficher
        questionTextView.setText(Html.fromHtml(processed.question.getQuestion()));

        // Gérer les manches
        GameEngine.MancheState mancheState = gameEngine.updateManches();
        if (mancheState.endMessage != null) {
            Toast.makeText(this, mancheState.endMessage, Toast.LENGTH_LONG).show();
        }
    }

    private void endGame() {
        repository.saveGameStats(totalQuestions, players.size());
        // Naviguer vers EndGameActivity
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        gameEngine.clearManches();
    }
}

4. Tests unitaires

Les tests sont situés dans app/src/test/java/.

4.1 Exécuter les tests

Depuis Android Studio :

  1. Clic droit sur la classe de test
  2. Select "Run 'ResultTest'"

Depuis la ligne de commande :

./gradlew testDebugUnitTest

Note : Avec Gradle 8.13, il y a un bug connu. Utilisez Android Studio ou un JDK 17-21.

4.2 Résultat des tests

Classe Tests Couverture
ResultTest 8 tests 100% Result
GameEngineTest 15 tests Logique jeu complète

5. Connexion Base de Données

5.1 DatabaseConnection

Classe moderne pour la connexion PostgreSQL (remplace l'obsolète AsyncTask).

Utilisation :

DatabaseConnection dbConnection = new DatabaseConnection();

dbConnection.connectAsync(new DatabaseConnection.ConnectionCallback() {
    @Override
    public void onSuccess(PGConnection connection) {
        // Connexion réussie - thread principal (UI)
        Log.d("Database", "Connecté !");
        // Utiliser la connexion...
    }

    @Override
    public void onFailure(Exception error) {
        // Erreur - thread principal (UI)
        Log.e("Database", "Erreur de connexion", error);
        Toast.makeText(context, "Erreur: " + error.getMessage(), Toast.LENGTH_SHORT).show();
    }
});

// Dans onDestroy()
@Override
protected void onDestroy() {
    super.onDestroy();
    dbConnection.shutdown(); // Libérer les ressources
}

5.2 Configuration

Les identifiants de connexion sont dans local.properties (non versionné) :

# Database credentials (DO NOT COMMIT)
db.url=jdbc:postgresql://your-host:5432/database
db.user=your-username
db.password=your-password

Ils sont injectés automatiquement dans BuildConfig par build.gradle.


6. Bonnes pratiques

6.1 Gestion des erreurs

// ✅ BON - Utiliser Result
Result<Data, Exception> result = repository.loadQuestions();
if (result.isSuccess()) {
    // Traiter les données
} else {
    // Gérer l'erreur
}

// ❌ Mauvais - Exceptions non gérées
try {
    Data data = repository.loadQuestions();
} catch (Exception e) {
    // Peut être oublié...
}

6.2 Séparation des responsabilités

// ✅ BON - Logique dans GameEngine
ProcessedQuestion processed = gameEngine.processQuestion(question, players, addedGorgees);

// ❌ Mauvais - Logique dans l'Activity
String text = question.getQuestion().replace("<J1>", players.get(0));
// ... logique compliquée dans l'UI

6.3 Cycle de vie

@Override
protected void onDestroy() {
    super.onDestroy();
    // Toujours nettoyer les ressources
    if (dbConnection != null) {
        dbConnection.shutdown();
    }
    if (gameEngine != null) {
        gameEngine.clearManches();
    }
}

7. Diagramme de flux

┌─────────────────┐
│   Jeux Activity │ (UI)
└────────┬────────┘
         │
         ├── loadQuestions() ──────► QuestionRepository
         │                                │
         │                                ├── Lire JSON
         │                                └── Retourner Result<Questions>
         │
         ├── processQuestion() ──────► GameEngine
         │                                │
         │                                ├── Remplacer <J1>, <J2>, etc.
         │                                ├── Remplacer <variante>
         │                                ├── Gérer <manches>
         │                                └── Retourner ProcessedQuestion
         │
         └── Afficher dans TextView

8. Migration depuis l'ancien code

Ancienne méthode (AsyncTask)

// Ancien code - À NE PAS UTILISER
new DatabaseConnection().execute();

@Override
protected void onPostExecute(Connection connection) {
    if (connection != null) {
        // ...
    }
}

Nouvelle méthode (ExecutorService + Callback)

// Nouveau code - Recommandé
DatabaseConnection db = new DatabaseConnection();
db.connectAsync(new ConnectionCallback() {
    @Override
    public void onSuccess(PGConnection connection) {
        // Succès
    }

    @Override
    public void onFailure(Exception error) {
        // Erreur
    }
});

9. Dépannage

Problème : Les tests unitaires échouent avec Gradle 8.13

Solution : Exécutez les tests depuis Android Studio, ou utilisez un JDK 17-21 au lieu de Java 24.

Problème : "Cannot resolve BuildConfig"

Solution : Faites un Build > Rebuild Project pour régénérer BuildConfig.

Problème : "QuestionRepository.QuestionLoadException"

Solution : Importez la classe interne :

import com.example.boidelov3.data.QuestionRepository.QuestionLoadException;

10. Ressources supplémentaires


Dernière mise à jour : 31 décembre 2025 Version : 3.0