(défis à durée). ");
+ sb.append("Sois créatif et varié !");
+
+ return sb.toString();
+ }
+
+ /**
+ * Parse la réponse de l'API (format OpenAI-compatible ou Anthropic)
+ */
+ private String parseResponse(String responseData, String format) {
+ try {
+ JSONObject json = new JSONObject(responseData);
+
+ if ("anthropic".equals(format)) {
+ // Format Anthropic/Z.ai
+ if (json.has("content")) {
+ JSONArray contentArray = json.getJSONArray("content");
+ if (contentArray.length() > 0) {
+ JSONObject firstContent = contentArray.getJSONObject(0);
+ if (firstContent.has("text")) {
+ return firstContent.getString("text").trim();
+ }
+ }
+ }
+ } else {
+ // Format OpenAI-compatible
+ JSONArray choices = json.getJSONArray("choices");
+ if (choices.length() > 0) {
+ JSONObject firstChoice = choices.getJSONObject(0);
+ JSONObject message = firstChoice.getJSONObject("message");
+ String content = message.getString("content");
+ // Nettoyer la réponse
+ return content.trim().replaceAll("^\"|\"$", "");
+ }
+ }
+ } catch (JSONException e) {
+ String operation = "Parsing de la réponse API " + provider.getDisplayName();
+ String details = "Format: " + format + ", Impossible de parser la réponse JSON";
+ Log.e("OpenAIService", operation + " - " + details, e);
+ }
+ return null;
+ }
+
+ /**
+ * Interface de callback pour les réponses API
+ */
+ public interface OpenAICallback {
+ void onSuccess(String question);
+ void onError(String errorMessage);
+ }
+
+ /**
+ * Libère les ressources
+ */
+ public void shutdown() {
+ if (client != null) {
+ client.dispatcher().executorService().shutdown();
+ client.connectionPool().evictAll();
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/PreferencesKeys.java b/app/src/main/java/com/example/boidelov3/PreferencesKeys.java
new file mode 100644
index 0000000..87fe749
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/PreferencesKeys.java
@@ -0,0 +1,57 @@
+package com.example.boidelov3;
+
+/**
+ * Centralise toutes les clés utilisées pour SharedPreferences.
+ * Cette classe garantit la cohérence des clés à travers l'application.
+ */
+public final class PreferencesKeys {
+
+ // Préfixe pour les noms de fichiers de préférences
+ public static final String PREFS_NAME_PLAYERS = "Joueurs";
+ public static final String PREFS_NAME_APP = "app";
+ public static final String PREFS_NAME_GAME_STATS = "game_stats";
+ public static final String PREFS_NAME_MY_PREFS = "MyPrefs";
+
+ // Clés pour les joueurs (stockés dans PREFS_NAME_PLAYERS)
+ public static final String KEY_PLAYER_1 = "J1";
+ public static final String KEY_PLAYER_2 = "J2";
+ public static final String KEY_PLAYER_3 = "J3";
+ // Pour les joueurs supplémentaires : J4, J5, etc. (généré dynamiquement)
+
+ // Clés pour les statistiques de jeu (stockées dans PREFS_NAME_GAME_STATS)
+ public static final String KEY_QUESTIONS_PLAYED = "questions_played";
+ public static final String KEY_PLAYERS_COUNT = "players_count";
+
+ // Clés pour l'état de l'application (stockées dans PREFS_NAME_APP)
+ public static final String KEY_ASKED_QUESTIONS = "askedQuestions";
+
+ // Clés pour les paramètres utilisateur (stockés dans PREFS_NAME_MY_PREFS)
+ public static final String KEY_SAVED_TEXT = "savedText";
+ public static final String KEY_AI_PROVIDER = "aiProvider";
+
+ // Clés pour la sauvegarde d'état (Bundle)
+ public static final String KEY_TOTAL_QUESTIONS_ASKED = "total_questions_asked";
+ public static final String KEY_CURRENT_QUESTION_TEXT = "current_question_text";
+ public static final String KEY_IS_MANCHE_ACTIVE = "is_manche_active";
+ public static final String KEY_MANCHES_COUNT = "manches_count";
+ public static final String KEY_MANCHE_IDS = "manche_ids";
+ public static final String KEY_MANCHE_COUNTS = "manche_counts";
+
+ // Constructeur privé pour empêcher l'instanciation
+ private PreferencesKeys() {
+ throw new AssertionError("Classe utilitaire, ne pas instancier");
+ }
+
+ /**
+ * Génère une clé de joueur pour les joueurs supplémentaires (J4, J5, etc.)
+ *
+ * @param playerNumber Le numéro du joueur (doit être >= 4)
+ * @return La clé générée (ex: "J4", "J5")
+ */
+ public static String getPlayerKey(int playerNumber) {
+ if (playerNumber < 1) {
+ throw new IllegalArgumentException("Le numéro de joueur doit être >= 1");
+ }
+ return "J" + playerNumber;
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/Question.java b/app/src/main/java/com/example/boidelov3/Question.java
index c21eff7..49e5324 100644
--- a/app/src/main/java/com/example/boidelov3/Question.java
+++ b/app/src/main/java/com/example/boidelov3/Question.java
@@ -2,6 +2,31 @@ package com.example.boidelov3;
import java.util.List;
+/**
+ * Représente une question du jeu Boidelo avec toutes ses propriétés.
+ *
+ * Cette classe contient toutes les informations nécessaires pour afficher
+ * et traiter une question lors du jeu.
+ *
+ * Propriétés principales :
+ *
+ * - {@code id} : Identifiant unique de la question
+ * - {@code question} : Texte de la question (peut contenir des balises)
+ * - {@code gorger} : Nombre de gorgées à boire/distribuer
+ * - {@code distribution} : Si vrai, le joueur distribue des gorgées
+ * - {@code recois} : Si vrai, le joueur boit des gorgées
+ * - {@code manches} : Si vrai, la question est un défi à manches
+ * - {@code caliente} : Si vrai, la question est spéciale/hot
+ * - {@code variante} : Liste des choix possibles pour une variante
+ *
+ *
+ * Balises spéciales dans le texte :
+ *
+ * - {@code }, {@code }, {@code } : Joueurs sélectionnés
+ * - {@code } : Nombre de manches pour un défi
+ * - {@code } : Choix à remplacer par une variante
+ *
+ */
public class Question {
private int id;
private String question;
@@ -10,16 +35,25 @@ public class Question {
private List variante;
private boolean recois;
private boolean manches;
+ private boolean caliente;
private String arret; // mise à jour du type de données
private int manchesRestantes; // pour le nombre de manches restantes
private String arretMessage; // pour le message d'arrêt
private String arretMessageManche; // pour le message d'arrêt pour les manches
- // Constructeur par défaut
+ /**
+ * Constructeur par défaut.
+ * Initialise tous les champs à leurs valeurs par défaut.
+ */
public Question() {
}
// Getters et setters pour tous les champs
+
+ /**
+ * Retourne l'identifiant unique de la question.
+ * @return L'ID de la question
+ */
public int getId() {
return id;
}
@@ -106,4 +140,12 @@ public class Question {
public void setArretMessageManche(String arretMessageManche) {
this.arretMessageManche = arretMessageManche;
}
+
+ public boolean isCaliente() {
+ return caliente;
+ }
+
+ public void setCaliente(boolean caliente) {
+ this.caliente = caliente;
+ }
}
diff --git a/app/src/main/java/com/example/boidelov3/data/PlayerStats.java b/app/src/main/java/com/example/boidelov3/data/PlayerStats.java
new file mode 100644
index 0000000..2c8862f
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/data/PlayerStats.java
@@ -0,0 +1,133 @@
+package com.example.boidelov3.data;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Classe pour suivre les statistiques d'un joueur pendant une partie.
+ *
+ * Cette classe est Parcelable pour pouvoir être passée entre les activités.
+ * Elle tracke deux types de statistiques :
+ *
+ * - {@code gorgeesBuves} : Nombre total de gorgées bues par le joueur
+ * - {@code gorgeesDistribuees} : Nombre total de gorgées distribuées par le joueur
+ *
+ *
+ * Exemple d'utilisation :
+ * {@code
+ * PlayerStats stats = new PlayerStats("Alice");
+ * stats.addGorgeesBuves(5);
+ * stats.addGorgeesDistribuees(3);
+ * int total = stats.getTotalGorgees(); // 8
+ * }
+ *
+ * SECURITY NOTE:
+ * Cette classe stocke des statistiques de jeu (gorgées, scores) qui ne sont PAS
+ * considérées comme des données sensibles. Aucun chiffrement n'est nécessaire.
+ *
+ * Si cette classe était étendue pour stocker des données personnelles (noms réels,
+ * emails, etc.), il faudrait utiliser :
+ *
+ * - AndroidX Security Library pour le chiffrement
+ * - EncryptedSharedPreferences pour le stockage persistant
+ *
+ */
+public class PlayerStats implements Parcelable {
+ private String playerName;
+ private int gorgeesBuves; // Nombre de gorgées bues par ce joueur
+ private int gorgeesDistribuees; // Nombre de gorgées distribuées par ce joueur
+
+ /**
+ * Crée les statistiques pour un joueur.
+ * Initialise les compteurs à zéro.
+ *
+ * @param playerName Le nom du joueur
+ */
+ public PlayerStats(String playerName) {
+ this.playerName = playerName;
+ this.gorgeesBuves = 0;
+ this.gorgeesDistribuees = 0;
+ }
+
+ // Constructor for Parcelable
+ protected PlayerStats(Parcel in) {
+ playerName = in.readString();
+ gorgeesBuves = in.readInt();
+ gorgeesDistribuees = in.readInt();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public PlayerStats createFromParcel(Parcel in) {
+ return new PlayerStats(in);
+ }
+
+ @Override
+ public PlayerStats[] newArray(int size) {
+ return new PlayerStats[size];
+ }
+ };
+
+ /**
+ * Retourne le nom du joueur.
+ * @return Le nom du joueur
+ */
+ public String getPlayerName() {
+ return playerName;
+ }
+
+ /**
+ * Retourne le nombre de gorgées bues par ce joueur.
+ * @return Le nombre de gorgées bues
+ */
+ public int getGorgeesBuves() {
+ return gorgeesBuves;
+ }
+
+ /**
+ * Retourne le nombre de gorgées distribuées par ce joueur.
+ * @return Le nombre de gorgées distribuées
+ */
+ public int getGorgeesDistribuees() {
+ return gorgeesDistribuees;
+ }
+
+ /**
+ * Ajoute des gorgées bues au total du joueur.
+ *
+ * @param count Le nombre de gorgées à ajouter (peut être négatif)
+ */
+ public void addGorgeesBuves(int count) {
+ this.gorgeesBuves += count;
+ }
+
+ /**
+ * Ajoute des gorgées distribuées au total du joueur.
+ *
+ * @param count Le nombre de gorgées à ajouter (peut être négatif)
+ */
+ public void addGorgeesDistribuees(int count) {
+ this.gorgeesDistribuees += count;
+ }
+
+ /**
+ * Retourne le total des gorgées (buves + distribuées).
+ *
+ * @return La somme des gorgées bues et distribuées
+ */
+ public int getTotalGorgees() {
+ return gorgeesBuves + gorgeesDistribuees;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(playerName);
+ dest.writeInt(gorgeesBuves);
+ dest.writeInt(gorgeesDistribuees);
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/data/QuestionCategory.java b/app/src/main/java/com/example/boidelov3/data/QuestionCategory.java
new file mode 100644
index 0000000..11605da
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/data/QuestionCategory.java
@@ -0,0 +1,155 @@
+package com.example.boidelov3.data;
+
+import com.example.boidelov3.Question;
+
+/**
+ * Catégories de questions avec leurs couleurs associées
+ * Permet de classer les questions et d'appliquer des fonds dynamiques
+ */
+public class QuestionCategory {
+
+ public enum Category {
+ CIBLAGE("Ciblage", "Questions qui ciblent un groupe spécifique", 0xFFFF6B6B), // Rouge doux
+ CLASSEMENT("Classement", "Vote pour élire le meilleur/pire", 0xFF4ECDC4), // Turquoise
+ JUGEMENT("Jugement", "J1 doit juger ou comparer des joueurs", 0xFFA8E6CF), // Menthe
+ DUEL("Duel J1/J2", "Compétition ou interaction entre 2 joueurs", 0xFFFFD93D), // Jaune
+ INTERACTIF("Interactif", "Quiz, devinettes, jeux de groupe", 0xFF6C5CE7), // Violet
+ DEFI_MANCHES("Défi", "Défi à manches avec durée limitée", 0xFF0984E3), // Bleu
+ VARIANTE("Variante", "Questions avec choix multiples", 0xFF00B894), // Vert menthe
+ CALIENTE("Caliente", "Questions chaudes/spéciales", 0xFFE84393), // Rouge vif
+ VOTE("Vote", "Vote à main levée", 0xFFFD79A8), // Rose
+ CLASSIQUE("Classique", "Question standard", 0xFFDFE6E9); // Gris clair
+
+ private final String name;
+ private final String description;
+ private final int color;
+
+ Category(String name, String description, int color) {
+ this.name = name;
+ this.description = description;
+ this.color = color;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getColor() {
+ return color;
+ }
+ }
+
+ /**
+ * Détecte la catégorie d'une question basée sur son contenu
+ */
+ public static Category detectCategory(Question question) {
+ if (question == null) {
+ return Category.CLASSIQUE;
+ }
+
+ String questionText = question.getQuestion().toLowerCase();
+
+ // 1. CALIENTE - Priorité haute
+ if (question.isCaliente()) {
+ return Category.CALIENTE;
+ }
+
+ // 2. DÉFI MANCHES - Questions avec
+ if (questionText.contains("") || question.isManches()) {
+ return Category.DEFI_MANCHES;
+ }
+
+ // 3. CIBLAGE - "Ceux qui", "Les joueurs qui", "Toutes celles", "Tous ceux"
+ if (questionText.startsWith("ceux qui") ||
+ questionText.startsWith("les joueurs qui") ||
+ questionText.startsWith("toutes celles") ||
+ questionText.startsWith("tous ceux") ||
+ questionText.startsWith("les joueurs de") ||
+ questionText.startsWith("celles") && questionText.contains("ont") ||
+ questionText.startsWith("les joueurs de") ||
+ questionText.startsWith("le groupe de")) {
+ return Category.CIBLAGE;
+ }
+
+ // 4. CLASSEMENT - "Le/La plus", "Élisez", "Le premier", "Qui a le plus"
+ if ((questionText.startsWith("le/là plus") ||
+ questionText.startsWith("le plus") ||
+ questionText.startsWith("la plus") ||
+ questionText.contains("élisez") ||
+ questionText.startsWith("le premier") ||
+ questionText.startsWith("la première") ||
+ questionText.startsWith("qui a le plus") ||
+ questionText.startsWith("celui/celle qui a le")) &&
+ !questionText.contains("")) {
+ return Category.CLASSEMENT;
+ }
+
+ // 5. VOTE - "Votez tous en même temps", "Vote à main levée"
+ if (questionText.contains("votez tous en même temps") ||
+ questionText.contains("vote à main levée") ||
+ questionText.contains("votez et le perdant")) {
+ return Category.VOTE;
+ }
+
+ // 6. JUGEMENT - " à toi de juger", "entre et ", "qui de ou "
+ if ((questionText.contains("juge") ||
+ questionText.contains("entre et ") ||
+ questionText.contains("qui de ou ") ||
+ questionText.contains("selon toi")) &&
+ questionText.contains("")) {
+ return Category.JUGEMENT;
+ }
+
+ // 7. DUEL J1/J2 - " et se regardent", " vs ", bras de fer, etc.
+ if ((questionText.contains(" et ") && !questionText.contains("variante")) ||
+ questionText.contains("bras de fer") ||
+ questionText.contains("clash") ||
+ questionText.contains("duel") ||
+ questionText.contains("concours")) {
+ return Category.DUEL;
+ }
+
+ // 8. INTERACTIF - Quiz, devinettes, mimes, karaoké, imitations
+ if (questionText.contains("quiz") ||
+ questionText.contains("devin") ||
+ questionText.contains("mime") ||
+ questionText.contains("karaoké") ||
+ questionText.contains("imitation") ||
+ questionText.contains("concours") ||
+ questionText.contains("doit inventer") ||
+ questionText.contains("doit créer") ||
+ questionText.contains("doit deviner") ||
+ questionText.contains("doit mimer") ||
+ questionText.contains("doit compléter") ||
+ questionText.contains("doit donner") && questionText.contains("compliment") ||
+ questionText.contains("doit nommer") && questionText.contains("qualité")) {
+ return Category.INTERACTIF;
+ }
+
+ // 9. VARIANTE - Questions avec
+ if (question.getVariante() != null && !question.getVariante().isEmpty()) {
+ return Category.VARIANTE;
+ }
+
+ // 10. CLASSIQUE - Par défaut
+ return Category.CLASSIQUE;
+ }
+
+ /**
+ * Retourne la couleur associée à une catégorie
+ */
+ public static int getColorForCategory(Category category) {
+ return category.getColor();
+ }
+
+ /**
+ * Retourne le nom de la catégorie
+ */
+ public static String getNameForCategory(Category category) {
+ return category.getName();
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/game/GameEngine.java b/app/src/main/java/com/example/boidelov3/game/GameEngine.java
index 195f740..8ca0bbe 100644
--- a/app/src/main/java/com/example/boidelov3/game/GameEngine.java
+++ b/app/src/main/java/com/example/boidelov3/game/GameEngine.java
@@ -57,11 +57,12 @@ public class GameEngine {
isManche = true;
}
- // Remplacer les joueurs
- questionText = replacePlayerPlaceholders(questionText, players);
+ // Remplacer les joueurs et récupérer le nombre pour l'accord
+ PlayerReplaceResult playerResult = replacePlayerPlaceholders(questionText, players);
+ questionText = playerResult.questionText;
- // Ajouter les gorgées
- questionText = addGorgeesText(question, questionText, addedGorgees);
+ // Ajouter les gorgées (en passant le nombre de joueurs pour l'accord)
+ questionText = addGorgeesText(question, questionText, addedGorgees, playerResult.playerCount);
// Mettre à jour la question avec le texte traité
Question resultQuestion = isManche ? activeManches.get(activeManches.size() - 1) : copyQuestion(question);
@@ -71,38 +72,55 @@ public class GameEngine {
}
/**
- * Remplace les placeholders de joueurs dans la question.
+ * Résultat du remplacement des joueurs avec le nombre de joueurs
*/
- private String replacePlayerPlaceholders(String questionText, List players) {
+ private static class PlayerReplaceResult {
+ String questionText;
+ int playerCount;
+
+ PlayerReplaceResult(String questionText, int playerCount) {
+ this.questionText = questionText;
+ this.playerCount = playerCount;
+ }
+ }
+
+ /**
+ * Remplace les placeholders de joueurs dans la question et retourne le nombre de joueurs.
+ */
+ private PlayerReplaceResult replacePlayerPlaceholders(String questionText, List players) {
boolean hasJ1 = questionText.contains("");
boolean hasJ2 = questionText.contains("");
boolean hasJ3 = questionText.contains("");
if (!hasJ1 && !hasJ2 && !hasJ3) {
- return questionText;
+ return new PlayerReplaceResult(questionText, 0);
}
List selectedPlayers = selectRandomPlayers(players, 3);
String result = questionText;
+ int playerCount = 0;
if (hasJ1 && hasJ2 && hasJ3 && selectedPlayers.size() >= 3) {
+ playerCount = 3;
result = result.replace("", selectedPlayers.get(0));
result = result.replace("", selectedPlayers.get(1));
result = result.replace("", selectedPlayers.get(2));
} else if (hasJ1 && hasJ2 && selectedPlayers.size() >= 2) {
+ playerCount = 2;
result = result.replace("", selectedPlayers.get(0));
result = result.replace("", selectedPlayers.get(1));
} else if (hasJ1 && selectedPlayers.size() >= 1) {
+ playerCount = 1;
result = result.replace("", selectedPlayers.get(0));
}
- return result;
+ return new PlayerReplaceResult(result, playerCount);
}
/**
* Ajoute le texte des gorgées à la question.
*/
- private String addGorgeesText(Question question, String questionText, int addedGorgees) {
+ private String addGorgeesText(Question question, String questionText, int addedGorgees, int playerCount) {
if (!question.isDistribution() && !question.isRecois()) {
return questionText;
}
@@ -112,13 +130,17 @@ public class GameEngine {
int totalGorgees = question.getGorger() + addedGorgees;
+ // Accord du verbe selon le nombre de joueurs
+ String boisVerb = (playerCount > 1) ? "boivent" : "boit";
+ String distribueVerb = (playerCount > 1) ? "distribuent" : "distribue";
+
// Déterminer si boire ou distribuer
if (question.isRecois() && question.isDistribution()) {
- sb.append(random.nextBoolean() ? "bois" : "distribue");
+ sb.append(random.nextBoolean() ? "" + boisVerb + "" : "" + distribueVerb + "");
} else if (question.isRecois()) {
- sb.append("bois");
+ sb.append("" + boisVerb + "");
} else {
- sb.append("distribue");
+ sb.append("" + distribueVerb + "");
}
sb.append(" ").append(totalGorgees).append(" gorgée");
diff --git a/app/src/main/java/com/example/boidelov3/utils/ErrorHandler.java b/app/src/main/java/com/example/boidelov3/utils/ErrorHandler.java
new file mode 100644
index 0000000..5538b05
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/utils/ErrorHandler.java
@@ -0,0 +1,99 @@
+package com.example.boidelov3.utils;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * Utilitaire centralisé pour la gestion des erreurs
+ * Fournit des méthodes cohérentes pour logger et afficher les erreurs
+ */
+public class ErrorHandler {
+
+ private static final String DEFAULT_TAG = "BoideloError";
+
+ /**
+ * Logger une erreur avec un TAG personnalisé
+ * @param tag Le tag pour les logs
+ * @param message Message descriptif de l'erreur
+ * @param throwable L'exception capturée
+ */
+ public static void logError(String tag, String message, Throwable throwable) {
+ Log.e(tag, message, throwable);
+ }
+
+ /**
+ * Logger une erreur avec le TAG par défaut
+ * @param message Message descriptif de l'erreur
+ * @param throwable L'exception capturée
+ */
+ public static void logError(String message, Throwable throwable) {
+ Log.e(DEFAULT_TAG, message, throwable);
+ }
+
+ /**
+ * Logger une erreur et afficher un Toast à l'utilisateur
+ * @param context Le contexte de l'application
+ * @param tag Le tag pour les logs
+ * @param logMessage Message technique pour les logs
+ * @param userMessage Message convivial pour l'utilisateur
+ * @param throwable L'exception capturée
+ */
+ public static void showError(Context context, String tag, String logMessage,
+ String userMessage, Throwable throwable) {
+ logError(tag, logMessage, throwable);
+ Toast.makeText(context, userMessage, Toast.LENGTH_LONG).show();
+ }
+
+ /**
+ * Logger une erreur et afficher un Toast à l'utilisateur (TAG par défaut)
+ * @param context Le contexte de l'application
+ * @param logMessage Message technique pour les logs
+ * @param userMessage Message convivial pour l'utilisateur
+ * @param throwable L'exception capturée
+ */
+ public static void showError(Context context, String logMessage,
+ String userMessage, Throwable throwable) {
+ showError(context, DEFAULT_TAG, logMessage, userMessage, throwable);
+ }
+
+ /**
+ * Logger une erreur sans afficher de Toast
+ * @param tag Le tag pour les logs
+ * @param message Message descriptif de l'erreur
+ * @param throwable L'exception capturée
+ */
+ public static void logErrorOnly(String tag, String message, Throwable throwable) {
+ logError(tag, message, throwable);
+ }
+
+ /**
+ * Créer un message d'erreur détaillé pour les logs avec contexte
+ * @param operation L'opération qui a échoué
+ * @param details Détails supplémentaires sur l'erreur
+ * @return Message formaté pour les logs
+ */
+ public static String buildErrorMessage(String operation, String details) {
+ if (details != null && !details.isEmpty()) {
+ return operation + " - " + details;
+ }
+ return operation;
+ }
+
+ /**
+ * Échapper une chaîne de caractères pour l'utiliser en toute sécurité dans HTML
+ * @param input La chaîne à échapper
+ * @return La chaîne échappée
+ */
+ public static String escapeHtml(String input) {
+ if (input == null) {
+ return "";
+ }
+ return input
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """)
+ .replace("'", "'");
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/utils/SecureConfig.java b/app/src/main/java/com/example/boidelov3/utils/SecureConfig.java
new file mode 100644
index 0000000..ebe4682
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/utils/SecureConfig.java
@@ -0,0 +1,220 @@
+package com.example.boidelov3.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+/**
+ * Classe utilitaire pour la gestion sécurisée des clés API et configuration sensible.
+ *
+ * SECURITY PRINCIPLES:
+ * - Les clés API ne sont JAMAIS stockées en dur dans le code
+ * - Utilise SharedPreferences chiffrés (doit être combiné avec AndroidX Security pour un chiffrage réel)
+ * - Valide les clés API avant utilisation
+ * - Fournit des méthodes pour nettoyer les données sensibles
+ *
+ * RECOMMANDATION: Pour une production sécurisée, utilisez AndroidX Security Library:
+ * implementation "androidx.security:security-crypto:1.1.0-alpha06"
+ * Et remplace les SharedPreferences par EncryptedSharedPreferences
+ */
+public class SecureConfig {
+
+ private static final String TAG = "SecureConfig";
+ private static final String PREFS_NAME = "SecureConfig";
+ private static final String KEY_API_KEY = "api_key_openai";
+ private static final String KEY_API_KEY_OPENROUTER = "api_key_openrouter";
+ private static final String KEY_API_KEY_ZAI = "api_key_zai";
+
+ private final SharedPreferences sharedPreferences;
+ private final SecureRandom secureRandom;
+
+ /**
+ * Constructeur
+ * @param context Contexte de l'application
+ */
+ public SecureConfig(Context context) {
+ // Pour plus de sécurité, utiliser EncryptedSharedPreferences d'AndroidX Security
+ // Pour l'instant, on utilise des SharedPreferences standards avec des avertissements
+ this.sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ this.secureRandom = new SecureRandom();
+ }
+
+ /**
+ * Sauvegarde une clé API de manière sécurisée
+ * NOTE: Pour une vraie sécurité, utilisez EncryptedSharedPreferences d'AndroidX Security
+ *
+ * @param provider Le fournisseur (openai, openrouter, zai)
+ * @param apiKey La clé API à stocker
+ * @return true si sauvegardé avec succès
+ */
+ public boolean saveApiKey(String provider, String apiKey) {
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ Log.w(TAG, "Tentative de sauvegarder une clé API vide");
+ return false;
+ }
+
+ // Valider la clé avant sauvegarde
+ if (!validateApiKeyFormat(provider, apiKey)) {
+ Log.w(TAG, "Format de clé API invalide pour " + provider);
+ return false;
+ }
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ String key = getPrefKeyForProvider(provider);
+ editor.putString(key, apiKey);
+ boolean success = editor.commit();
+
+ if (success) {
+ Log.i(TAG, "Clé API sauvegardée pour " + provider);
+ } else {
+ Log.e(TAG, "Erreur lors de la sauvegarde de la clé API");
+ }
+
+ return success;
+ }
+
+ /**
+ * Récupère une clé API stockée
+ *
+ * @param provider Le fournisseur (openai, openrouter, zai)
+ * @return La clé API ou null si non trouvée
+ */
+ public String getApiKey(String provider) {
+ String key = getPrefKeyForProvider(provider);
+ return sharedPreferences.getString(key, null);
+ }
+
+ /**
+ * Supprime une clé API stockée
+ *
+ * @param provider Le fournisseur
+ * @return true si supprimée avec succès
+ */
+ public boolean removeApiKey(String provider) {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ String key = getPrefKeyForProvider(provider);
+ editor.remove(key);
+ boolean success = editor.commit();
+
+ if (success) {
+ Log.i(TAG, "Clé API supprimée pour " + provider);
+ }
+
+ return success;
+ }
+
+ /**
+ * Supprime toutes les clés API stockées
+ * Utiliser cette méthode lors de la déconnexion ou pour nettoyer les données
+ */
+ public void clearAllApiKeys() {
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.remove(KEY_API_KEY);
+ editor.remove(KEY_API_KEY_OPENROUTER);
+ editor.remove(KEY_API_KEY_ZAI);
+ editor.apply();
+ Log.i(TAG, "Toutes les clés API ont été supprimées");
+ }
+
+ /**
+ * Vérifie si une clé API existe pour un provider
+ *
+ * @param provider Le fournisseur
+ * @return true si une clé existe
+ */
+ public boolean hasApiKey(String provider) {
+ String key = getPrefKeyForProvider(provider);
+ return sharedPreferences.contains(key) && sharedPreferences.getString(key, null) != null;
+ }
+
+ /**
+ * Valide le format d'une clé API selon le provider
+ *
+ * @param provider Le fournisseur (openai, openrouter, zai)
+ * @param apiKey La clé API à valider
+ * @return true si le format est valide
+ */
+ public boolean validateApiKeyFormat(String provider, String apiKey) {
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ return false;
+ }
+
+ String trimmedKey = apiKey.trim();
+
+ switch (provider.toLowerCase()) {
+ case "openai":
+ // Les clés OpenAI commencent par "sk-"
+ return trimmedKey.startsWith("sk-") && trimmedKey.length() >= 20;
+
+ case "openrouter":
+ // Les clés OpenRouter commencent par "sk-or-"
+ return trimmedKey.startsWith("sk-or-") && trimmedKey.length() >= 20;
+
+ case "zai":
+ // Les clés Z.ai/Anthropic commencent par "sk-ant-"
+ return trimmedKey.startsWith("sk-ant-") && trimmedKey.length() >= 20;
+
+ default:
+ Log.w(TAG, "Provider inconnu: " + provider);
+ return false;
+ }
+ }
+
+ /**
+ * Génère un hash sécurisé d'une clé pour vérification (stockage local)
+ * Ne stocke JAMAIS la clé en clair dans les logs
+ *
+ * @param apiKey La clé API
+ * @return Le hash de la clé
+ */
+ public String hashApiKey(String apiKey) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(apiKey.getBytes("UTF-8"));
+ return Base64.getEncoder().encodeToString(hash);
+ } catch (Exception e) {
+ Log.e(TAG, "Erreur lors du hash de la clé", e);
+ return null;
+ }
+ }
+
+ /**
+ * Génère une chaîne aléatoire sécurisée pour utilisation comme nonce ou token
+ *
+ * @param length Longueur de la chaîne
+ * @return Chaîne aléatoire sécurisée
+ */
+ public String generateSecureToken(int length) {
+ byte[] token = new byte[length];
+ secureRandom.nextBytes(token);
+ return Base64.getEncoder().encodeToString(token);
+ }
+
+ /**
+ * Retourne la clé SharedPreferences appropriée selon le provider
+ */
+ private String getPrefKeyForProvider(String provider) {
+ switch (provider.toLowerCase()) {
+ case "openrouter":
+ return KEY_API_KEY_OPENROUTER;
+ case "zai":
+ return KEY_API_KEY_ZAI;
+ case "openai":
+ default:
+ return KEY_API_KEY;
+ }
+ }
+
+ /**
+ * Vérifie si des clés API sont stockées (pour vérifier la configuration)
+ *
+ * @return true si au moins une clé API est configurée
+ */
+ public boolean isAnyApiKeyConfigured() {
+ return hasApiKey("openai") || hasApiKey("openrouter") || hasApiKey("zai");
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/utils/SoundGenerator.java b/app/src/main/java/com/example/boidelov3/utils/SoundGenerator.java
new file mode 100644
index 0000000..d2c224a
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/utils/SoundGenerator.java
@@ -0,0 +1,134 @@
+package com.example.boidelov3.utils;
+
+import android.media.AudioAttributes;
+import android.media.SoundPool;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+/**
+ * Générateur de sons utilisant ToneGenerator
+ * Permet de créer des sons sans fichiers audio externes
+ */
+public class SoundGenerator {
+ private android.media.ToneGenerator toneGenerator;
+ private Handler handler;
+ private boolean isMuted = false;
+
+ public SoundGenerator() {
+ // Volume: 0-100
+ toneGenerator = new android.media.ToneGenerator(
+ android.media.AudioManager.STREAM_MUSIC, 80
+ );
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Son de clic - court et léger
+ */
+ public void playClick() {
+ if (isMuted) return;
+ new Thread(() -> {
+ try {
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_BEEP,
+ 50
+ );
+ } catch (Exception e) {
+ Log.e("SoundGenerator", "Erreur lors de la lecture du son de clic", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Son de succès - mélange ascendant
+ */
+ public void playSuccess() {
+ if (isMuted) return;
+ new Thread(() -> {
+ try {
+ Thread.sleep(0);
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_NACK,
+ 100
+ );
+ Thread.sleep(120);
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_ACK,
+ 150
+ );
+ } catch (Exception e) {
+ Log.e("SoundGenerator", "Erreur lors de la lecture du son de succès", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Son de manche - dramatique pour annoncer un défi
+ */
+ public void playManche() {
+ if (isMuted) return;
+ new Thread(() -> {
+ try {
+ // Premier ton grave
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_BEEP2,
+ 200
+ );
+ Thread.sleep(250);
+ // Deuxième ton plus aigu
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_BEEP,
+ 300
+ );
+ } catch (Exception e) {
+ Log.e("SoundGenerator", "Erreur lors de la lecture du son de manche", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Son de fin - célébration
+ */
+ public void playFin() {
+ if (isMuted) return;
+ new Thread(() -> {
+ try {
+ // Séquence festive
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_ACK,
+ 150
+ );
+ Thread.sleep(180);
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_NACK,
+ 150
+ );
+ Thread.sleep(180);
+ toneGenerator.startTone(
+ android.media.ToneGenerator.TONE_PROP_ACK,
+ 250
+ );
+ } catch (Exception e) {
+ Log.e("SoundGenerator", "Erreur lors de la lecture du son de fin", e);
+ }
+ }).start();
+ }
+
+ /**
+ * Activer/Désactiver le son
+ */
+ public void setMuted(boolean muted) {
+ this.isMuted = muted;
+ }
+
+ /**
+ * Libérer les ressources
+ */
+ public void release() {
+ if (toneGenerator != null) {
+ toneGenerator.release();
+ toneGenerator = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/boidelov3/utils/SoundManager.java b/app/src/main/java/com/example/boidelov3/utils/SoundManager.java
new file mode 100644
index 0000000..9431fea
--- /dev/null
+++ b/app/src/main/java/com/example/boidelov3/utils/SoundManager.java
@@ -0,0 +1,87 @@
+package com.example.boidelov3.utils;
+
+import android.content.Context;
+
+/**
+ * Gestionnaire de sons pour l'application
+ * Utilise SoundGenerator pour créer des sons sans fichiers externes
+ */
+public class SoundManager {
+ private static SoundManager instance;
+ private SoundGenerator soundGenerator;
+ private Context context;
+
+ /**
+ * Obtient l'instance unique du SoundManager (Singleton)
+ */
+ public static synchronized SoundManager getInstance(Context context) {
+ if (instance == null) {
+ instance = new SoundManager(context.getApplicationContext());
+ }
+ return instance;
+ }
+
+ /**
+ * Constructeur privé
+ */
+ private SoundManager(Context context) {
+ this.context = context;
+ this.soundGenerator = new SoundGenerator();
+ }
+
+ /**
+ * Joue le son de clic
+ */
+ public void playClick() {
+ if (soundGenerator != null) {
+ soundGenerator.playClick();
+ }
+ }
+
+ /**
+ * Joue le son de succès
+ */
+ public void playSuccess() {
+ if (soundGenerator != null) {
+ soundGenerator.playSuccess();
+ }
+ }
+
+ /**
+ * Joue le son de manche (nouveau défi)
+ */
+ public void playManche() {
+ if (soundGenerator != null) {
+ soundGenerator.playManche();
+ }
+ }
+
+ /**
+ * Joue le son de fin de partie
+ */
+ public void playFin() {
+ if (soundGenerator != null) {
+ soundGenerator.playFin();
+ }
+ }
+
+ /**
+ * Active ou désactive le son
+ */
+ public void setMuted(boolean muted) {
+ if (soundGenerator != null) {
+ soundGenerator.setMuted(muted);
+ }
+ }
+
+ /**
+ * Libère les ressources
+ */
+ public void release() {
+ if (soundGenerator != null) {
+ soundGenerator.release();
+ soundGenerator = null;
+ }
+ instance = null;
+ }
+}
diff --git a/app/src/main/res/layout/activity_end_game.xml b/app/src/main/res/layout/activity_end_game.xml
index d31616a..ad54f9b 100644
--- a/app/src/main/res/layout/activity_end_game.xml
+++ b/app/src/main/res/layout/activity_end_game.xml
@@ -220,6 +220,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_jeux.xml b/app/src/main/res/layout/activity_jeux.xml
index df6ed73..6234bdf 100644
--- a/app/src/main/res/layout/activity_jeux.xml
+++ b/app/src/main/res/layout/activity_jeux.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/game_background"
+ android:id="@+id/rootLayout"
android:fitsSystemWindows="true"
tools:context=".Jeux">
diff --git a/app/src/main/res/layout/activity_jeux_parametres.xml b/app/src/main/res/layout/activity_jeux_parametres.xml
index 006a40f..920651a 100644
--- a/app/src/main/res/layout/activity_jeux_parametres.xml
+++ b/app/src/main/res/layout/activity_jeux_parametres.xml
@@ -232,7 +232,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
- android:visibility="gone"
app:cardBackgroundColor="@color/card_background"
app:cardCornerRadius="16dp"
app:cardElevation="4dp"
@@ -276,14 +275,35 @@
+
+
+
+
+
+
+
"Suivant !"
Paramètres du jeu
Commencer à vous mettre une mine !
- Activer les questions par ChatGPT
- Clé API OpenAI
- OpenAI [En cours de développement]
- Test de Connectivité Openai
+ Activer les questions par IA
+ Clé API
+ Intelligence Artificielle
+ Tester la connexion
\ No newline at end of file
diff --git a/app/src/test/java/com/example/boidelov3/QuestionTest.java b/app/src/test/java/com/example/boidelov3/QuestionTest.java
new file mode 100644
index 0000000..d5f884e
--- /dev/null
+++ b/app/src/test/java/com/example/boidelov3/QuestionTest.java
@@ -0,0 +1,218 @@
+package com.example.boidelov3;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests unitaires pour la classe Question.
+ * Couvre les getters/setters et les cas limites.
+ */
+public class QuestionTest {
+
+ @Test
+ public void testDefaultConstructor_createsEmptyQuestion() {
+ Question question = new Question();
+
+ assertEquals("ID should be 0 by default", 0, question.getId());
+ assertNull("Question text should be null by default", question.getQuestion());
+ assertEquals("Gorger should be 0 by default", 0, question.getGorger());
+ assertFalse("Distribution should be false by default", question.isDistribution());
+ assertFalse("Recois should be false by default", question.isRecois());
+ assertFalse("Manches should be false by default", question.isManches());
+ assertFalse("Caliente should be false by default", question.isCaliente());
+ assertNull("Arret should be null by default", question.getArret());
+ assertNull("Variante should be null by default", question.getVariante());
+ }
+
+ @Test
+ public void testSetId_getId_returnsCorrectValue() {
+ Question question = new Question();
+ question.setId(42);
+
+ assertEquals("ID should be 42", 42, question.getId());
+ }
+
+ @Test
+ public void testSetQuestion_getQuestion_returnsCorrectValue() {
+ Question question = new Question();
+ String testQuestion = "Test question text";
+ question.setQuestion(testQuestion);
+
+ assertEquals("Question text should match", testQuestion, question.getQuestion());
+ }
+
+ @Test
+ public void testSetGorger_getGorger_returnsCorrectValue() {
+ Question question = new Question();
+ question.setGorger(5);
+
+ assertEquals("Gorger should be 5", 5, question.getGorger());
+ }
+
+ @Test
+ public void testSetDistribution_isDistribution_returnsCorrectValue() {
+ Question question = new Question();
+ question.setDistribution(true);
+
+ assertTrue("Distribution should be true", question.isDistribution());
+ }
+
+ @Test
+ public void testSetRecois_isRecois_returnsCorrectValue() {
+ Question question = new Question();
+ question.setRecois(true);
+
+ assertTrue("Recois should be true", question.isRecois());
+ }
+
+ @Test
+ public void testSetManches_isManches_returnsCorrectValue() {
+ Question question = new Question();
+ question.setManches(true);
+
+ assertTrue("Manches should be true", question.isManches());
+ }
+
+ @Test
+ public void testSetCaliente_isCaliente_returnsCorrectValue() {
+ Question question = new Question();
+ question.setCaliente(true);
+
+ assertTrue("Caliente should be true", question.isCaliente());
+ }
+
+ @Test
+ public void testSetArret_getArret_returnsCorrectValue() {
+ Question question = new Question();
+ String arretText = "Arrêtez maintenant !";
+ question.setArret(arretText);
+
+ assertEquals("Arret text should match", arretText, question.getArret());
+ }
+
+ @Test
+ public void testSetManchesRestantes_getManchesRestantes_returnsCorrectValue() {
+ Question question = new Question();
+ question.setManchesRestantes(10);
+
+ assertEquals("ManchesRestantes should be 10", 10, question.getManchesRestantes());
+ }
+
+ @Test
+ public void testSetArretMessage_getArretMessage_returnsCorrectValue() {
+ Question question = new Question();
+ String message = "Fin du défi !";
+ question.setArretMessage(message);
+
+ assertEquals("ArretMessage should match", message, question.getArretMessage());
+ }
+
+ @Test
+ public void testSetArretMessageManche_getArretMessageManche_returnsCorrectValue() {
+ Question question = new Question();
+ String message = "Fin de défi\nArrêtez maintenant !";
+ question.setArretMessageManche(message);
+
+ assertEquals("ArretMessageManche should match", message, question.getArretMessageManche());
+ }
+
+ @Test
+ public void testSetVariante_getVariante_returnsCorrectValue() {
+ Question question = new Question();
+ List variantes = Arrays.asList("Variante 1", "Variante 2", "Variante 3");
+ question.setVariante(variantes);
+
+ assertEquals("Variante list should match", variantes, question.getVariante());
+ assertEquals("Variante list size should be 3", 3, question.getVariante().size());
+ }
+
+ @Test
+ public void testSetVariante_withEmptyList_returnsEmptyList() {
+ Question question = new Question();
+ List emptyList = Arrays.asList();
+ question.setVariante(emptyList);
+
+ assertNotNull("Variante should not be null", question.getVariante());
+ assertTrue("Variante list should be empty", question.getVariante().isEmpty());
+ }
+
+ @Test
+ public void testSetVariante_withNull_acceptsNull() {
+ Question question = new Question();
+ question.setVariante(null);
+
+ assertNull("Variante should be null", question.getVariante());
+ }
+
+ @Test
+ public void testCompleteQuestion_withAllFields() {
+ Question question = new Question();
+ question.setId(100);
+ question.setQuestion("Question complète");
+ question.setGorger(3);
+ question.setDistribution(true);
+ question.setRecois(false);
+ question.setManches(true);
+ question.setCaliente(false);
+ question.setArret("Stop !");
+ question.setManchesRestantes(5);
+ question.setArretMessage("Message");
+ question.setArretMessageManche("Message manche");
+ question.setVariante(Arrays.asList("V1", "V2"));
+
+ assertEquals("ID should be 100", 100, question.getId());
+ assertEquals("Question should match", "Question complète", question.getQuestion());
+ assertEquals("Gorger should be 3", 3, question.getGorger());
+ assertTrue("Distribution should be true", question.isDistribution());
+ assertFalse("Recois should be false", question.isRecois());
+ assertTrue("Manches should be true", question.isManches());
+ assertFalse("Caliente should be false", question.isCaliente());
+ assertEquals("Arret should match", "Stop !", question.getArret());
+ assertEquals("ManchesRestantes should be 5", 5, question.getManchesRestantes());
+ assertEquals("ArretMessage should match", "Message", question.getArretMessage());
+ assertEquals("ArretMessageManche should match", "Message manche", question.getArretMessageManche());
+ assertEquals("Variante size should be 2", 2, question.getVariante().size());
+ }
+
+ @Test
+ public void testQuestionWithZeroGorger() {
+ Question question = new Question();
+ question.setGorger(0);
+
+ assertEquals("Gorger should be 0", 0, question.getGorger());
+ }
+
+ @Test
+ public void testQuestionWithNegativeManchesRestantes() {
+ Question question = new Question();
+ question.setManchesRestantes(-1);
+
+ assertEquals("ManchesRestantes should be -1", -1, question.getManchesRestantes());
+ }
+
+ @Test
+ public void testQuestionWithLargeId() {
+ Question question = new Question();
+ int largeId = 999999;
+ question.setId(largeId);
+
+ assertEquals("ID should handle large values", largeId, question.getId());
+ }
+
+ @Test
+ public void testMultipleSetters_chainingWorks() {
+ Question question = new Question();
+ question.setId(1);
+ question.setQuestion("Test");
+ question.setGorger(2);
+ question.setDistribution(true);
+
+ assertEquals("All setters should work independently", 1, question.getId());
+ assertEquals("Question should be preserved", "Test", question.getQuestion());
+ assertEquals("Gorger should be preserved", 2, question.getGorger());
+ assertTrue("Distribution should be preserved", question.isDistribution());
+ }
+}
diff --git a/app/src/test/java/com/example/boidelov3/data/PlayerStatsTest.java b/app/src/test/java/com/example/boidelov3/data/PlayerStatsTest.java
new file mode 100644
index 0000000..23eda4e
--- /dev/null
+++ b/app/src/test/java/com/example/boidelov3/data/PlayerStatsTest.java
@@ -0,0 +1,216 @@
+package com.example.boidelov3.data;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests unitaires pour la classe PlayerStats.
+ * Couvre les statistiques de joueurs, les opérations arithmétiques et Parcelable.
+ */
+public class PlayerStatsTest {
+
+ private PlayerStats playerStats;
+ private static final String TEST_PLAYER_NAME = "Alice";
+
+ @Before
+ public void setUp() {
+ playerStats = new PlayerStats(TEST_PLAYER_NAME);
+ }
+
+ @Test
+ public void testConstructor_initializesWithZeroStats() {
+ assertEquals("Player name should match", TEST_PLAYER_NAME, playerStats.getPlayerName());
+ assertEquals("Initial gorgeesBuves should be 0", 0, playerStats.getGorgeesBuves());
+ assertEquals("Initial gorgeesDistribuees should be 0", 0, playerStats.getGorgeesDistribuees());
+ assertEquals("Initial total should be 0", 0, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testGetPlayerName_returnsCorrectName() {
+ assertEquals("Player name should be Alice", TEST_PLAYER_NAME, playerStats.getPlayerName());
+ }
+
+ @Test
+ public void testGetGorgeesBuves_initialValue() {
+ assertEquals("Initial gorgeesBuves should be 0", 0, playerStats.getGorgeesBuves());
+ }
+
+ @Test
+ public void testAddGorgeesBuves_incrementsCount() {
+ playerStats.addGorgeesBuves(5);
+ assertEquals("GorgeesBuves should be 5", 5, playerStats.getGorgeesBuves());
+
+ playerStats.addGorgeesBuves(3);
+ assertEquals("GorgeesBuves should be 8", 8, playerStats.getGorgeesBuves());
+ }
+
+ @Test
+ public void testAddGorgeesBuves_withZero_doesNotChange() {
+ playerStats.addGorgeesBuves(5);
+ playerStats.addGorgeesBuves(0);
+ assertEquals("GorgeesBuves should remain 5", 5, playerStats.getGorgeesBuves());
+ }
+
+ @Test
+ public void testAddGorgeesBuves_withNegativeValue_allowsNegative() {
+ playerStats.addGorgeesBuves(5);
+ playerStats.addGorgeesBuves(-2);
+ assertEquals("GorgeesBuves should be 3", 3, playerStats.getGorgeesBuves());
+ }
+
+ @Test
+ public void testGetGorgeesDistribuees_initialValue() {
+ assertEquals("Initial gorgeesDistribuees should be 0", 0, playerStats.getGorgeesDistribuees());
+ }
+
+ @Test
+ public void testAddGorgeesDistribuees_incrementsCount() {
+ playerStats.addGorgeesDistribuees(7);
+ assertEquals("GorgeesDistribuees should be 7", 7, playerStats.getGorgeesDistribuees());
+
+ playerStats.addGorgeesDistribuees(2);
+ assertEquals("GorgeesDistribuees should be 9", 9, playerStats.getGorgeesDistribuees());
+ }
+
+ @Test
+ public void testAddGorgeesDistribuees_withZero_doesNotChange() {
+ playerStats.addGorgeesDistribuees(10);
+ playerStats.addGorgeesDistribuees(0);
+ assertEquals("GorgeesDistribuees should remain 10", 10, playerStats.getGorgeesDistribuees());
+ }
+
+ @Test
+ public void testGetTotalGorgees_withOnlyBuves() {
+ playerStats.addGorgeesBuves(5);
+ assertEquals("Total should be 5", 5, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testGetTotalGorgees_withOnlyDistribuees() {
+ playerStats.addGorgeesDistribuees(3);
+ assertEquals("Total should be 3", 3, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testGetTotalGorgees_withBoth() {
+ playerStats.addGorgeesBuves(5);
+ playerStats.addGorgeesDistribuees(3);
+ assertEquals("Total should be 8", 8, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testGetTotalGorgees_withZeros() {
+ assertEquals("Total should be 0 when no stats", 0, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testGetTotalGorgees_afterMultipleOperations() {
+ playerStats.addGorgeesBuves(10);
+ playerStats.addGorgeesDistribuees(5);
+ playerStats.addGorgeesBuves(3);
+ playerStats.addGorgeesDistribuees(2);
+
+ assertEquals("Total should be 20", 20, playerStats.getTotalGorgees());
+ assertEquals("GorgeesBuves should be 13", 13, playerStats.getGorgeesBuves());
+ assertEquals("GorgeesDistribuees should be 7", 7, playerStats.getGorgeesDistribuees());
+ }
+
+ @Test
+ public void testParcelable_CREATOR_notNull() {
+ assertNotNull("CREATOR should not be null", PlayerStats.CREATOR);
+ }
+
+ @Test
+ public void testParcelable_writeAndRead() {
+ playerStats.addGorgeesBuves(15);
+ playerStats.addGorgeesDistribuees(8);
+
+ Parcel parcel = Parcel.obtain();
+ playerStats.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ PlayerStats restored = PlayerStats.CREATOR.createFromParcel(parcel);
+
+ assertEquals("Player name should match", TEST_PLAYER_NAME, restored.getPlayerName());
+ assertEquals("GorgeesBuves should match", 15, restored.getGorgeesBuves());
+ assertEquals("GorgeesDistribuees should match", 8, restored.getGorgeesDistribuees());
+ assertEquals("Total should match", 23, restored.getTotalGorgees());
+ }
+
+ @Test
+ public void testParcelable_newArray() {
+ PlayerStats[] array = PlayerStats.CREATOR.newArray(5);
+ assertEquals("Array length should be 5", 5, array.length);
+ assertNotNull("Array elements should not be null", array);
+ for (PlayerStats stats : array) {
+ assertNull("Array elements should be null initially", stats);
+ }
+ }
+
+ @Test
+ public void testDescribeContents() {
+ assertEquals("describeContents should return 0", 0, playerStats.describeContents());
+ }
+
+ @Test
+ public void testParcelable_withZeroStats() {
+ Parcel parcel = Parcel.obtain();
+ playerStats.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ PlayerStats restored = PlayerStats.CREATOR.createFromParcel(parcel);
+
+ assertEquals("Player name should match", TEST_PLAYER_NAME, restored.getPlayerName());
+ assertEquals("GorgeesBuves should be 0", 0, restored.getGorgeesBuves());
+ assertEquals("GorgeesDistribuees should be 0", 0, restored.getGorgeesDistribuees());
+ }
+
+ @Test
+ public void testMultiplePlayers_haveIndependentStats() {
+ PlayerStats player1 = new PlayerStats("Alice");
+ PlayerStats player2 = new PlayerStats("Bob");
+
+ player1.addGorgeesBuves(5);
+ player2.addGorgeesBuves(3);
+ player1.addGorgeesDistribuees(2);
+ player2.addGorgeesDistribuees(4);
+
+ assertEquals("Alice stats should be independent", 7, player1.getTotalGorgees());
+ assertEquals("Bob stats should be independent", 7, player2.getTotalGorgees());
+ assertEquals("Alice gorgeesBuves should be 5", 5, player1.getGorgeesBuves());
+ assertEquals("Bob gorgeesBuves should be 3", 3, player2.getGorgeesBuves());
+ }
+
+ @Test
+ public void testLargeValues() {
+ playerStats.addGorgeesBuves(1000);
+ playerStats.addGorgeesDistribuees(500);
+
+ assertEquals("Should handle large values", 1500, playerStats.getTotalGorgees());
+ }
+
+ @Test
+ public void testConstructor_withDifferentNames() {
+ PlayerStats alice = new PlayerStats("Alice");
+ PlayerStats bob = new PlayerStats("Bob");
+ PlayerStats charlie = new PlayerStats("Charlie");
+
+ assertEquals("Alice", alice.getPlayerName());
+ assertEquals("Bob", bob.getPlayerName());
+ assertEquals("Charlie", charlie.getPlayerName());
+ }
+
+ @Test
+ public void testStatsDoNotInterfere() {
+ playerStats.addGorgeesBuves(10);
+ assertEquals(10, playerStats.getGorgeesBuves());
+ assertEquals(0, playerStats.getGorgeesDistribuees());
+
+ playerStats.addGorgeesDistribuees(5);
+ assertEquals(10, playerStats.getGorgeesBuves()); // Should not change
+ assertEquals(5, playerStats.getGorgeesDistribuees());
+ }
+}
diff --git a/app/src/test/java/com/example/boidelov3/data/QuestionCategoryTest.java b/app/src/test/java/com/example/boidelov3/data/QuestionCategoryTest.java
new file mode 100644
index 0000000..fd25939
--- /dev/null
+++ b/app/src/test/java/com/example/boidelov3/data/QuestionCategoryTest.java
@@ -0,0 +1,285 @@
+package com.example.boidelov3.data;
+
+import com.example.boidelov3.Question;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+/**
+ * Tests unitaires pour la classe QuestionCategory.
+ * Couvre la détection automatique de catégorie et les énumérations.
+ */
+public class QuestionCategoryTest {
+
+ /**
+ * Crée une question avec le texte spécifié
+ */
+ private Question createQuestion(String text) {
+ Question q = new Question();
+ q.setQuestion(text);
+ return q;
+ }
+
+ @Test
+ public void testDetectCategory_withNull_returnsClassique() {
+ QuestionCategory.Category category = QuestionCategory.detectCategory(null);
+ assertEquals("Null question should return CLASSIQUE", QuestionCategory.Category.CLASSIQUE, category);
+ }
+
+ @Test
+ public void testDetectCategory_calienteFlag_returnsCaliente() {
+ Question q = createQuestion("Question simple");
+ q.setCaliente(true);
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Caliente flag should return CALIENTE", QuestionCategory.Category.CALIENTE, category);
+ }
+
+ @Test
+ public void testDetectCategory_manches_returnsDefiManches() {
+ Question q = createQuestion("Défi à manches ");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Question with should return DEFI_MANCHES", QuestionCategory.Category.DEFI_MANCHES, category);
+ }
+
+ @Test
+ public void testDetectCategory_manchesFlag_returnsDefiManches() {
+ Question q = createQuestion("Défi sans tag");
+ q.setManches(true);
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Manches flag should return DEFI_MANCHES", QuestionCategory.Category.DEFI_MANCHES, category);
+ }
+
+ @Test
+ public void testDetectCategory_ciblage_ceuxQui() {
+ Question q = createQuestion("Ceux qui portent du rouge boivent 2 gorgées");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CIBLAGE pattern", QuestionCategory.Category.CIBLAGE, category);
+ }
+
+ @Test
+ public void testDetectCategory_ciblage_lesJoueursQui() {
+ Question q = createQuestion("Les joueurs qui ont des lunettes distribuent 3 gorgées");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CIBLAGE pattern", QuestionCategory.Category.CIBLAGE, category);
+ }
+
+ @Test
+ public void testDetectCategory_ciblage_toutesCelles() {
+ Question q = createQuestion("Toutes celles qui ont les cheveux longs boivent");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CIBLAGE pattern", QuestionCategory.Category.CIBLAGE, category);
+ }
+
+ @Test
+ public void testDetectCategory_ciblage_tousCeux() {
+ Question q = createQuestion("Tous ceux qui sont nés en hiver");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CIBLAGE pattern", QuestionCategory.Category.CIBLAGE, category);
+ }
+
+ @Test
+ public void testDetectCategory_classement_lePlus() {
+ Question q = createQuestion("Le plus ivre boit 3 gorgées");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CLASSEMENT pattern", QuestionCategory.Category.CLASSEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_classement_laPlus() {
+ Question q = createQuestion("La plus drôle distribue");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CLASSEMENT pattern", QuestionCategory.Category.CLASSEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_classement_elisez() {
+ Question q = createQuestion("Élisez le meilleur joueur");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CLASSEMENT pattern", QuestionCategory.Category.CLASSEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_classement_quiALePlus() {
+ Question q = createQuestion("Qui a le plus bu distribue 5 gorgées");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect CLASSEMENT pattern", QuestionCategory.Category.CLASSEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_vote_votezTous() {
+ Question q = createQuestion("Votez tous en même temps pour le perdant");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect VOTE pattern", QuestionCategory.Category.VOTE, category);
+ }
+
+ @Test
+ public void testDetectCategory_vote_mainLevee() {
+ Question q = createQuestion("Vote à main levée");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect VOTE pattern", QuestionCategory.Category.VOTE, category);
+ }
+
+ @Test
+ public void testDetectCategory_jugement_juge() {
+ Question q = createQuestion(" à toi de juger qui distribue");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect JUGEMENT pattern", QuestionCategory.Category.JUGEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_jugement_selonToi() {
+ Question q = createQuestion(", selon toi qui mérite de boire ?");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect JUGEMENT pattern", QuestionCategory.Category.JUGEMENT, category);
+ }
+
+ @Test
+ public void testDetectCategory_duel_j1EtJ2() {
+ Question q = createQuestion(" et se regardent dans les yeux");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect DUEL pattern", QuestionCategory.Category.DUEL, category);
+ }
+
+ @Test
+ public void testDetectCategory_interactif_quiz() {
+ Question q = createQuestion("Quiz : quel est le plus grand fleuve du monde ?");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect INTERACTIF pattern", QuestionCategory.Category.INTERACTIF, category);
+ }
+
+ @Test
+ public void testDetectCategory_interactif_deviner() {
+ Question q = createQuestion(" doit deviner la chanson");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect INTERACTIF pattern", QuestionCategory.Category.INTERACTIF, category);
+ }
+
+ @Test
+ public void testDetectCategory_interactif_mime() {
+ Question q = createQuestion(" doit mimer un animal");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect INTERACTIF pattern", QuestionCategory.Category.INTERACTIF, category);
+ }
+
+ @Test
+ public void testDetectCategory_variante_withVariante() {
+ Question q = createQuestion("Choisissez une option ");
+ q.setVariante(Arrays.asList("Option A", "Option B"));
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Should detect VARIANTE pattern", QuestionCategory.Category.VARIANTE, category);
+ }
+
+ @Test
+ public void testDetectCategory_variante_emptyList() {
+ Question q = createQuestion("Test question");
+ q.setVariante(Arrays.asList());
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Empty variante should return CLASSIQUE", QuestionCategory.Category.CLASSIQUE, category);
+ }
+
+ @Test
+ public void testDetectCategory_default_returnsClassique() {
+ Question q = createQuestion("Question simple sans pattern particulier");
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("Default should return CLASSIQUE", QuestionCategory.Category.CLASSIQUE, category);
+ }
+
+ @Test
+ public void testGetColorForCategory_returnsValidColor() {
+ for (QuestionCategory.Category category : QuestionCategory.Category.values()) {
+ int color = QuestionCategory.getColorForCategory(category);
+ assertTrue("Color should be positive for " + category, color > 0);
+ assertTrue("Color should be <= 0xFFFFFF for " + category, color <= 0xFFFFFF);
+ }
+ }
+
+ @Test
+ public void testGetNameForCategory_returnsNonEmpty() {
+ for (QuestionCategory.Category category : QuestionCategory.Category.values()) {
+ String name = QuestionCategory.getNameForCategory(category);
+ assertNotNull("Name should not be null for " + category, name);
+ assertFalse("Name should not be empty for " + category, name.isEmpty());
+ }
+ }
+
+ @Test
+ public void testCategoryEnum_allCategoriesHaveUniqueNames() {
+ java.util.Set names = new java.util.HashSet<>();
+ for (QuestionCategory.Category category : QuestionCategory.Category.values()) {
+ assertTrue("Duplicate name found: " + category.getName(),
+ names.add(category.getName()));
+ }
+ }
+
+ @Test
+ public void testCategoryEnum_allCategoriesHaveUniqueColors() {
+ java.util.Set colors = new java.util.HashSet<>();
+ for (QuestionCategory.Category category : QuestionCategory.Category.values()) {
+ assertTrue("Duplicate color found for " + category.getName(),
+ colors.add(category.getColor()));
+ }
+ }
+
+ @Test
+ public void testDetectCategory_caseInsensitive() {
+ Question q1 = createQuestion("CEUX QUI ont un chapeau boivent");
+ Question q2 = createQuestion("ceux qui ont un chapeau boivent");
+
+ QuestionCategory.Category cat1 = QuestionCategory.detectCategory(q1);
+ QuestionCategory.Category cat2 = QuestionCategory.detectCategory(q2);
+
+ assertEquals("Detection should be case-insensitive", cat1, cat2);
+ assertEquals("Should detect CIBLAGE", QuestionCategory.Category.CIBLAGE, cat1);
+ }
+
+ @Test
+ public void testDetectCategory_priority_calienteOverOthers() {
+ Question q = createQuestion(" et se font un bras de fer");
+ q.setCaliente(true);
+
+ QuestionCategory.Category category = QuestionCategory.detectCategory(q);
+ assertEquals("CALIENTE should have priority", QuestionCategory.Category.CALIENTE, category);
+ }
+
+ @Test
+ public void testDetectCategory_priority_manchesOverVariante() {
+ Question q = createQuestion("Défi avec choix