fix(classic): anti-répétition persistante + cohérence des questions

Anti-répétition:
- Chaque question posée est marquée en SharedPreferences (par ID):
  elle ne revient plus, même entre les parties, tant que le stock
  de 185 questions n'est pas épuisé (reset automatique ensuite)
- Le fallback ne renvoie plus une question déjà posée

Cohérence (audit croisé JSON <-> moteur, 185 questions):
- Accord du verbe selon le sujet réel: 62 questions à sujet pluriel
  recevaient «boit» au lieu de «boivent» (isPluralSubject)
- Remplacement indépendant des balises <J1>/<J2>/<J3>: une question
  contenant <J2> sans <J1> n'affichait plus la balise brute
- Le champ arret du JSON est désormais utilisé comme message de fin
  de défi (27 messages étaient des données mortes)
- id 84: champ "distribut" (typo) jamais lu -> question muette
- 16 questions à double consigne (gorgées en dur + gorgées du moteur)
  passées en auto-suffisantes
- Balises <variante> manquantes (ids 21, 97) rétablies
- 78 corrections de langue/accord dans les textes (orthographe,
  accords m/f, variantes vieux/vieille, branché(e), fins de phrases
  incohérentes avec le verbe ajouté...)
This commit is contained in:
Roman
2026-08-19 18:41:07 +00:00
parent b3722ed776
commit de85af4e2a
4 changed files with 245 additions and 142 deletions
@@ -98,6 +98,13 @@ public class QuestionRepository {
return Result.success(selected);
}
/**
* Retourne l'ensemble des IDs des questions déjà posées (toutes parties confondues).
*/
public Set<String> getAskedQuestionIds() {
return new HashSet<>(prefs.getStringSet(KEY_ASKED_QUESTIONS, new HashSet<>()));
}
/**
* Marque une question comme étant posée.
*/
@@ -100,23 +100,25 @@ public class GameEngine {
String result = questionText;
int playerCount = 0;
if (hasJ1 && hasJ2 && hasJ3 && selectedPlayers.size() >= 3) {
playerCount = 3;
// Chaque balise est remplacée indépendamment : une question contenant
// uniquement <J2> ou <J3> reste jouable (l'ordre J1, J2, J3 est conservé)
if (hasJ1 && selectedPlayers.size() >= 1) {
playerCount++;
result = result.replace("<J1>", selectedPlayers.get(0));
}
if (hasJ2 && selectedPlayers.size() >= 2) {
playerCount++;
result = result.replace("<J2>", selectedPlayers.get(1));
}
if (hasJ3 && selectedPlayers.size() >= 3) {
playerCount++;
result = result.replace("<J3>", selectedPlayers.get(2));
} else if (hasJ1 && hasJ2 && selectedPlayers.size() >= 2) {
playerCount = 2;
result = result.replace("<J1>", selectedPlayers.get(0));
result = result.replace("<J2>", selectedPlayers.get(1));
} else if (hasJ1 && selectedPlayers.size() >= 1) {
playerCount = 1;
result = result.replace("<J1>", selectedPlayers.get(0));
}
return new PlayerReplaceResult(result, playerCount);
}
/**
* Ajoute le texte des gorgées à la question.
*/
@@ -130,9 +132,11 @@ 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";
// Accord du verbe selon le véritable sujet de la phrase
boolean pluriel = isPluralSubject(questionText);
String boisVerb = pluriel ? "boivent" : "boit";
String distribueVerb = pluriel ? "distribuent" : "distribue";
// Déterminer si boire ou distribuer
if (question.isRecois() && question.isDistribution()) {
@@ -152,6 +156,31 @@ public class GameEngine {
return sb.toString();
}
/**
* Indique si le sujet de la question est pluriel, pour accorder le verbe
* (boit/boivent, distribue/distribuent) ajouté en fin de question.
*/
private static boolean isPluralSubject(String questionText) {
String t = questionText.trim().toLowerCase();
if (t.isEmpty()) {
return false;
}
// Sujet de groupe en tête de question : « Ceux qui… », « Les fans de… », « Tous ceux qui… »
if (t.startsWith("ceux") || t.startsWith("celles") || t.startsWith("tous ") || t.startsWith("toutes ")
|| t.startsWith("les ")) {
return true;
}
// Sujet pluriel en toute fin de phrase, juste avant le verbe ajouté
String[] finsPlurielles = {"perdants", "joueurs", "joueuses", "gens", "ceux", "celles", "eux", "elles",
"abonnés", "abonnées", "végans", "célibataires", "filles", "garçons", "couples"};
for (String fin : finsPlurielles) {
if (t.endsWith(fin)) {
return true;
}
}
return false;
}
/**
* Sélectionne n joueurs aléatoires uniques.
*/
@@ -191,6 +191,24 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
if (result.isSuccess()) {
Questions q = result.getData();
questions = q != null && q.getQuestions() != null ? q.getQuestions() : new ArrayList<>();
// Exclure les questions déjà posées lors des parties précédentes,
// pour qu'une question ne revienne pas tant que tout le stock n'a pas été épuisé
java.util.Set<String> askedIds = questionRepository.getAskedQuestionIds();
if (!askedIds.isEmpty()) {
List<Question> unasked = new ArrayList<>();
for (Question question : questions) {
if (!askedIds.contains(String.valueOf(question.getId()))) {
unasked.add(question);
}
}
if (unasked.isEmpty()) {
// Toutes les questions ont été posées : on repart sur le stock complet
questionRepository.resetAskedQuestions();
} else {
questions = unasked;
}
}
} else {
android.util.Log.e("BoideloClassicGame", "Erreur lors du chargement des questions: " + result.getError());
questions = new ArrayList<>();
@@ -407,10 +425,19 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
}
askedQuestionIndices.add(questionIndex);
// Persistance : la question ne reviendra plus lors des prochaines parties
questionRepository.markQuestionAsAsked(question.getId());
return question;
}
// Si on ne trouve pas de question valide, retourner une question aléatoire
// Si on ne trouve pas de question valide (toutes les restantes ont <manches>),
// retourner une question non posée plutôt qu'une question au hasard
if (!availableIndices.isEmpty()) {
int fallbackIndex = availableIndices.get(random.nextInt(availableIndices.size()));
askedQuestionIndices.add(fallbackIndex);
questionRepository.markQuestionAsAsked(questions.get(fallbackIndex).getId());
return questions.get(fallbackIndex);
}
return questions.get(random.nextInt(questions.size()));
}
@@ -825,8 +852,10 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
question.setManchesRestantes(nbaleatoiremanches);
// Définir le message de fin de défi
String stopMessage = "Fin de défi!\n" + questionText;
question.setArretMessageManche(stopMessage);
// Message de fin de défi : texte d'arrêt dédié s'il existe, sinon rappel de la consigne
String arret = question.getArret();
String stopMessage = "Fin de défi !\n" + (arret != null && !arret.trim().isEmpty() ? arret : questionText);
questionsAvecManches.add(question);
@@ -873,24 +902,25 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
if (isJoueurs1 || isJoueurs2 || isJoueurs3) {
List<String> aleatoirejoueurs = TroisJoueurAleatoire();
if (isJoueurs1 && isJoueurs2 && isJoueurs3 && aleatoirejoueurs.size() >= RANDOM_PLAYER_SELECTION_COUNT) {
playerCount = 3;
j1Name = aleatoirejoueurs.get(0);
questionText = questionText.replace("<J1>", ErrorHandler.escapeHtml(j1Name));
questionText = questionText.replace("<J2>", ErrorHandler.escapeHtml(aleatoirejoueurs.get(1)));
questionText = questionText.replace("<J3>", ErrorHandler.escapeHtml(aleatoirejoueurs.get(2)));
} else if (isJoueurs1 && isJoueurs2 && aleatoirejoueurs.size() >= TWO_PLAYERS) {
playerCount = 2;
j1Name = aleatoirejoueurs.get(0);
questionText = questionText.replace("<J1>", ErrorHandler.escapeHtml(j1Name));
questionText = questionText.replace("<J2>", ErrorHandler.escapeHtml(aleatoirejoueurs.get(1)));
} else if (isJoueurs1 && aleatoirejoueurs.size() >= ONE_PLAYER) {
playerCount = 1;
// Chaque balise est remplacée indépendamment : une question contenant
// uniquement <J2> ou <J3> reste jouable (l'ordre J1, J2, J3 est conservé)
if (isJoueurs1 && aleatoirejoueurs.size() >= ONE_PLAYER) {
playerCount++;
j1Name = aleatoirejoueurs.get(0);
questionText = questionText.replace("<J1>", ErrorHandler.escapeHtml(j1Name));
}
if (isJoueurs2 && aleatoirejoueurs.size() >= TWO_PLAYERS) {
playerCount++;
questionText = questionText.replace("<J2>", ErrorHandler.escapeHtml(aleatoirejoueurs.get(1)));
}
if (isJoueurs3 && aleatoirejoueurs.size() >= RANDOM_PLAYER_SELECTION_COUNT) {
playerCount++;
questionText = questionText.replace("<J3>", ErrorHandler.escapeHtml(aleatoirejoueurs.get(2)));
}
}
return new PlayerSelectionResult(questionText, j1Name, playerCount);
}
@@ -949,15 +979,19 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
}
/**
* Détermine l'action (boit/boivent ou distribue/distribuent) selon le nombre de joueurs
/**
* Détermine l'action (boit/boivent ou distribue/distribuent) selon le véritable
* sujet de la phrase, pour un accord grammatical correct.
*/
private ActionChoiceResult determineActionChoice(Question question, String questionText, int playerCount) {
boolean isBois = false;
boolean isDistribue = false;
// Accord du verbe selon le nombre de joueurs
String boisVerb = (playerCount > 1) ? "boivent" : "boit";
String distribueVerb = (playerCount > 1) ? "distribuent" : "distribue";
// Accord du verbe selon le sujet réel de la question
boolean pluriel = isPluralSubject(questionText);
String boisVerb = pluriel ? "boivent" : "boit";
String distribueVerb = pluriel ? "distribuent" : "distribue";
if (question.isRecois() && question.isDistribution()) {
boolean rand = random.nextBoolean();
@@ -979,6 +1013,31 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
return new ActionChoiceResult(questionText, isBois, isDistribue);
}
/**
* Indique si le sujet de la question est pluriel, pour accorder le verbe
* (boit/boivent, distribue/distribuent) ajouté en fin de question.
*/
private boolean isPluralSubject(String questionText) {
String t = questionText.trim().toLowerCase();
if (t.isEmpty()) {
return false;
}
// Sujet de groupe en tête de question : « Ceux qui… », « Les fans de… », « Tous ceux qui… »
if (t.startsWith("ceux") || t.startsWith("celles") || t.startsWith("tous ") || t.startsWith("toutes ")
|| t.startsWith("les ")) {
return true;
}
// Sujet pluriel en toute fin de phrase, juste avant le verbe ajouté
String[] finsPlurielles = {"perdants", "joueurs", "joueuses", "gens", "ceux", "celles", "eux", "elles",
"abonnés", "abonnées", "végans", "célibataires", "filles", "garçons", "couples"};
for (String fin : finsPlurielles) {
if (t.endsWith(fin)) {
return true;
}
}
return false;
}
/**
* Ajoute le nombre de gorgées au texte de la question
*/