modernisation api et test unitaire

This commit is contained in:
2025-12-31 12:46:03 +01:00
parent c5772eef95
commit 9bcbc79706
9 changed files with 1328 additions and 28 deletions
@@ -1,41 +1,77 @@
package com.example.boidelov3;
import android.os.AsyncTask;
import com.example.boidelov3.BuildConfig;
import android.os.Handler;
import android.os.Looper;
import com.example.boidelov3.BuildConfig;
import com.impossibl.postgres.api.jdbc.PGConnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Gère la connexion à la base de données PostgreSQL de manière asynchrone.
* Remplace l'obsolète AsyncTask par ExecutorService + Handler.
*/
public class DatabaseConnection {
public class DatabaseConnection extends AsyncTask<Void, Void, PGConnection> {
private static final String DB_URL = BuildConfig.DB_URL;
private static final String USER = BuildConfig.DB_USER;
private static final String PASSWORD = BuildConfig.DB_PASSWORD;
@Override
protected PGConnection doInBackground(Void... params) {
PGConnection connection = null;
try {
// Code de connexion à la base de données PostgreSQL
String url = DB_URL;
String username = USER;
String password = PASSWORD;
connection = (PGConnection) DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
private final ExecutorService executorService;
private final Handler mainHandler;
public DatabaseConnection() {
this.executorService = Executors.newSingleThreadExecutor();
this.mainHandler = new Handler(Looper.getMainLooper());
}
protected void onPostExecute(Connection connection) {
// Traitez le résultat de la connexion ici
if (connection != null) {
// Connexion réussie
} else {
// Échec de la connexion
}
/**
* Interface de callback pour le résultat de la connexion.
*/
public interface ConnectionCallback {
void onSuccess(PGConnection connection);
void onFailure(Exception error);
}
}
/**
* Connexion asynchrone à la base de données.
*
* @param callback Callback appelé sur le thread principal
*/
public void connectAsync(ConnectionCallback callback) {
executorService.execute(() -> {
PGConnection connection = null;
Exception error = null;
try {
connection = (PGConnection) DriverManager.getConnection(DB_URL, USER, PASSWORD);
} catch (SQLException e) {
error = e;
}
// Retour sur le thread principal pour notifier le résultat
final PGConnection finalConnection = connection;
final Exception finalError = error;
mainHandler.post(() -> {
if (finalError == null && finalConnection != null) {
callback.onSuccess(finalConnection);
} else {
callback.onFailure(finalError != null ? finalError : new SQLException("Connection failed"));
}
});
});
}
/**
* Ferme l'executorService pour libérer les ressources.
* À appeler dans onDestroy() de l'Activity.
*/
public void shutdown() {
executorService.shutdown();
}
}
@@ -54,7 +54,26 @@ public class Jeuxold extends AppCompatActivity {
//Parti OpenAI ; keyOpenai ; ratiOpenai, openAI
//new DatabaseConnection().execute();
// Ancienne AsyncTask (obsolète) :
// new DatabaseConnection().execute();
// Nouvelle API avec callback (recommandé) :
// DatabaseConnection dbConnection = new DatabaseConnection();
// dbConnection.connectAsync(new DatabaseConnection.ConnectionCallback() {
// @Override
// public void onSuccess(PGConnection connection) {
// // Connexion réussie - utiliser la connexion ici
// Log.d("Database", "Connected successfully");
// }
//
// @Override
// public void onFailure(Exception error) {
// // Erreur de connexion
// Log.e("Database", "Connection failed", error);
// Toast.makeText(Jeuxold.this, "Erreur de connexion", Toast.LENGTH_SHORT).show();
// }
// });
// N'oubliez pas d'appeler dbConnection.shutdown() dans onDestroy()
// if(openAI) {
// ChatGPTTask chatGPTTask = new ChatGPTTask( this, keyOpenai);
@@ -0,0 +1,142 @@
package com.example.boidelov3.data;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.example.boidelov3.Question;
import com.example.boidelov3.Questions;
import com.google.gson.Gson;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Repository pour gérer l'accès aux données des questions.
* Sépare la logique de chargement et de stockage de l'UI.
*/
public class QuestionRepository {
private static final String TAG = "QuestionRepository";
private static final String PREFS_NAME = "app";
private static final String KEY_ASKED_QUESTIONS = "askedQuestions";
private final Context context;
private final SharedPreferences prefs;
private final Gson gson;
private Questions cachedQuestions;
public QuestionRepository(Context context) {
this.context = context.getApplicationContext();
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
}
/**
* Charge toutes les questions depuis le fichier JSON.
*
* @return Result contenant les questions ou une erreur
*/
public Result<Questions, QuestionLoadException> loadQuestions() {
if (cachedQuestions != null) {
return Result.success(cachedQuestions);
}
try {
InputStream is = context.getAssets().open("question.json");
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, "UTF-8");
cachedQuestions = gson.fromJson(json, Questions.class);
if (cachedQuestions == null || cachedQuestions.getQuestions() == null) {
return Result.failure(new QuestionLoadException("Impossible de parser le JSON"));
}
Log.d(TAG, "Chargé " + cachedQuestions.getQuestions().size() + " questions");
return Result.success(cachedQuestions);
} catch (IOException e) {
Log.e(TAG, "Erreur de chargement des questions", e);
return Result.failure(new QuestionLoadException("Erreur de lecture du fichier questions.json", e));
}
}
/**
* Obtient une question aléatoire qui n'a pas encore été posée.
*
* @param questions La liste complète des questions
* @return Une question ou null si toutes ont été posées
*/
public Result<Question, QuestionLoadException> getRandomUnaskedQuestion(List<Question> questions) {
Set<String> askedQuestions = prefs.getStringSet(KEY_ASKED_QUESTIONS, new HashSet<>());
List<Question> unaskedQuestions = new ArrayList<>();
for (Question question : questions) {
if (!askedQuestions.contains(String.valueOf(question.getId()))) {
unaskedQuestions.add(question);
}
}
if (unaskedQuestions.isEmpty()) {
return Result.failure(new QuestionLoadException("Toutes les questions ont été posées"));
}
// Sélection aléatoire
java.util.Random random = new java.util.Random();
Question selected = unaskedQuestions.get(random.nextInt(unaskedQuestions.size()));
// Marquer comme posée
askedQuestions.add(String.valueOf(selected.getId()));
prefs.edit().putStringSet(KEY_ASKED_QUESTIONS, askedQuestions).apply();
return Result.success(selected);
}
/**
* Marque une question comme étant posée.
*/
public void markQuestionAsAsked(int questionId) {
Set<String> askedQuestions = prefs.getStringSet(KEY_ASKED_QUESTIONS, new HashSet<>());
askedQuestions.add(String.valueOf(questionId));
prefs.edit().putStringSet(KEY_ASKED_QUESTIONS, askedQuestions).apply();
}
/**
* Réinitialise la liste des questions posées.
*/
public void resetAskedQuestions() {
prefs.edit().remove(KEY_ASKED_QUESTIONS).apply();
Log.d(TAG, "Questions posées réinitialisées");
}
/**
* Sauvegarde les statistiques d'une partie.
*/
public void saveGameStats(int questionsPlayed, int playersCount) {
SharedPreferences statsPrefs = context.getSharedPreferences("game_stats", Context.MODE_PRIVATE);
statsPrefs.edit()
.putInt("questions_played", questionsPlayed)
.putInt("players_count", playersCount)
.apply();
Log.d(TAG, "Stats sauvegardées: " + questionsPlayed + " questions, " + playersCount + " joueurs");
}
/**
* Exception personnalisée pour les erreurs de chargement.
*/
public static class QuestionLoadException extends Exception {
public QuestionLoadException(String message) {
super(message);
}
public QuestionLoadException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -0,0 +1,74 @@
package com.example.boidelov3.data;
/**
* Wrapper pour représenter le résultat d'une opération qui peut échouer.
* Remplace les exceptions par des valeurs de retour typées.
*
* @param <T> Le type de donnée en cas de succès
* @param <E> Le type d'erreur en cas d'échec
*/
public class Result<T, E extends Exception> {
private final T data;
private final E error;
private Result(T data, E error) {
this.data = data;
this.error = error;
}
/**
* Crée un résultat réussi
*/
public static <T, E extends Exception> Result<T, E> success(T data) {
return new Result<>(data, null);
}
/**
* Crée un résultat d'erreur
*/
public static <T, E extends Exception> Result<T, E> failure(E error) {
return new Result<>(null, error);
}
/**
* @return true si l'opération a réussi
*/
public boolean isSuccess() {
return error == null;
}
/**
* @return true si l'opération a échoué
*/
public boolean isFailure() {
return error != null;
}
/**
* @return les données en cas de succès, null sinon
*/
public T getData() {
return data;
}
/**
* @return l'erreur en cas d'échec, null sinon
*/
public E getError() {
return error;
}
/**
* @return les données ou null si échec
*/
public T getOrNull() {
return data;
}
/**
* @return les données ou une valeur par défaut
*/
public T getOrElse(T defaultValue) {
return isSuccess() ? data : defaultValue;
}
}
@@ -0,0 +1,236 @@
package com.example.boidelov3.game;
import com.example.boidelov3.Question;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* Moteur de jeu contenant toute la logique métier.
* Indépendant de l'UI et du framework Android.
*/
public class GameEngine {
private final Random random;
private final List<Question> activeManches;
public GameEngine() {
this.random = new Random();
this.activeManches = new ArrayList<>();
}
/**
* Traite une question en remplaçant les variables.
*
* @param question La question à traiter
* @param players La liste des joueurs
* @param addedGorgees Nombre de gorgées additionnelles à ajouter
* @return La question traitée avec les variables remplacées
*/
public ProcessedQuestion processQuestion(Question question, List<String> players, int addedGorgees) {
String questionText = question.getQuestion();
boolean isManche = false;
// Remplacer les variantes
if (question.getVariante() != null && !question.getVariante().isEmpty()) {
String chosenVariante = question.getVariante().get(random.nextInt(question.getVariante().size()));
questionText = questionText.replace("<variante>", chosenVariante);
}
// Gérer les manches
if (questionText.contains("<manches>")) {
int manchesCount = random.nextInt(10) + 5;
questionText = questionText.replace("<manches>", String.valueOf(manchesCount));
// Créer une copie de la question pour la manche active
Question mancheQuestion = copyQuestion(question);
mancheQuestion.setQuestion(questionText); // Question avec le nombre de manches
mancheQuestion.setManchesRestantes(manchesCount);
// Message de fin de manche
String stopMessage = question.getArret() != null ? question.getArret() : "Fin du défi!";
mancheQuestion.setArretMessageManche("Fin de défi!\n" + stopMessage);
activeManches.add(mancheQuestion);
isManche = true;
}
// Remplacer les joueurs
questionText = replacePlayerPlaceholders(questionText, players);
// Ajouter les gorgées
questionText = addGorgeesText(question, questionText, addedGorgees);
// Mettre à jour la question avec le texte traité
Question resultQuestion = isManche ? activeManches.get(activeManches.size() - 1) : copyQuestion(question);
resultQuestion.setQuestion(questionText);
return new ProcessedQuestion(resultQuestion, isManche);
}
/**
* Remplace les placeholders de joueurs dans la question.
*/
private String replacePlayerPlaceholders(String questionText, List<String> players) {
boolean hasJ1 = questionText.contains("<J1>");
boolean hasJ2 = questionText.contains("<J2>");
boolean hasJ3 = questionText.contains("<J3>");
if (!hasJ1 && !hasJ2 && !hasJ3) {
return questionText;
}
List<String> selectedPlayers = selectRandomPlayers(players, 3);
String result = questionText;
if (hasJ1 && hasJ2 && hasJ3 && selectedPlayers.size() >= 3) {
result = result.replace("<J1>", selectedPlayers.get(0));
result = result.replace("<J2>", selectedPlayers.get(1));
result = result.replace("<J3>", selectedPlayers.get(2));
} else if (hasJ1 && hasJ2 && selectedPlayers.size() >= 2) {
result = result.replace("<J1>", selectedPlayers.get(0));
result = result.replace("<J2>", selectedPlayers.get(1));
} else if (hasJ1 && selectedPlayers.size() >= 1) {
result = result.replace("<J1>", selectedPlayers.get(0));
}
return result;
}
/**
* Ajoute le texte des gorgées à la question.
*/
private String addGorgeesText(Question question, String questionText, int addedGorgees) {
if (!question.isDistribution() && !question.isRecois()) {
return questionText;
}
StringBuilder sb = new StringBuilder(questionText);
sb.append(" ");
int totalGorgees = question.getGorger() + addedGorgees;
// Déterminer si boire ou distribuer
if (question.isRecois() && question.isDistribution()) {
sb.append(random.nextBoolean() ? "<b>bois</b>" : "<b>distribue</b>");
} else if (question.isRecois()) {
sb.append("<b>bois</b>");
} else {
sb.append("<b>distribue</b>");
}
sb.append(" ").append(totalGorgees).append(" gorgée");
if (totalGorgees > 1) {
sb.append("s");
}
sb.append(".");
return sb.toString();
}
/**
* Sélectionne n joueurs aléatoires uniques.
*/
public List<String> selectRandomPlayers(List<String> allPlayers, int count) {
Set<String> selected = new HashSet<>();
int available = Math.min(count, allPlayers.size());
while (selected.size() < available) {
selected.add(allPlayers.get(random.nextInt(allPlayers.size())));
}
return new ArrayList<>(selected);
}
/**
* Met à jour les manches actives et retourne l'état.
*
* @return MancheState contenant la manche à afficher ou null si terminée
*/
public MancheState updateManches() {
if (activeManches.isEmpty()) {
return new MancheState(null, false, null);
}
Question mancheQuestion = activeManches.get(0);
mancheQuestion.setManchesRestantes(mancheQuestion.getManchesRestantes() - 1);
if (mancheQuestion.getManchesRestantes() <= 0) {
// Manche terminée
String endMessage = mancheQuestion.getArretMessageManche();
activeManches.remove(0);
return new MancheState(null, false, endMessage);
} else {
// Manche toujours active
return new MancheState(mancheQuestion, true, null);
}
}
/**
* @return true si une manche est active
*/
public boolean hasActiveManche() {
return !activeManches.isEmpty();
}
/**
* @return le nombre de manches actives
*/
public int getActiveManchesCount() {
return activeManches.size();
}
/**
* Vide toutes les manches actives.
*/
public void clearManches() {
activeManches.clear();
}
/**
* Crée une copie d'une question (pour éviter de modifier l'original).
*/
private Question copyQuestion(Question source) {
Question copy = new Question();
copy.setId(source.getId());
copy.setQuestion(source.getQuestion());
copy.setArret(source.getArret());
copy.setVariante(source.getVariante());
copy.setDistribution(source.isDistribution());
copy.setRecois(source.isRecois());
copy.setGorger(source.getGorger());
copy.setManchesRestantes(source.getManchesRestantes());
copy.setArretMessageManche(source.getArretMessageManche());
return copy;
}
/**
* Conteneur pour une question traitée.
*/
public static class ProcessedQuestion {
public final Question question;
public final boolean isManche;
public ProcessedQuestion(Question question, boolean isManche) {
this.question = question;
this.isManche = isManche;
}
}
/**
* État des manches après mise à jour.
*/
public static class MancheState {
public final Question activeManche;
public final boolean hasManche;
public final String endMessage;
public MancheState(Question activeManche, boolean hasManche, String endMessage) {
this.activeManche = activeManche;
this.hasManche = hasManche;
this.endMessage = endMessage;
}
}
}
@@ -0,0 +1,79 @@
package com.example.boidelov3.data;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests unitaires pour la classe Result.
*/
public class ResultTest {
@Test
public void testSuccess_createsSuccessfulResult() {
Result<String, Exception> result = Result.success("test data");
assertTrue("Result should be success", result.isSuccess());
assertFalse("Result should not be failure", result.isFailure());
assertEquals("Data should match", "test data", result.getData());
assertNull("Error should be null", result.getError());
}
@Test
public void testFailure_createsFailedResult() {
Exception error = new Exception("test error");
Result<String, Exception> result = Result.failure(error);
assertFalse("Result should not be success", result.isSuccess());
assertTrue("Result should be failure", result.isFailure());
assertNull("Data should be null", result.getData());
assertEquals("Error should match", error, result.getError());
}
@Test
public void testGetOrNull_returnsDataWhenSuccess() {
Result<String, Exception> result = Result.success("test data");
assertEquals("Should return data", "test data", result.getOrNull());
}
@Test
public void testGetOrNull_returnsNullWhenFailure() {
Result<String, Exception> result = Result.failure(new Exception("error"));
assertNull("Should return null", result.getOrNull());
}
@Test
public void testGetOrElse_returnsDataWhenSuccess() {
Result<String, Exception> result = Result.success("test data");
assertEquals("Should return data", "test data", result.getOrElse("default"));
}
@Test
public void testGetOrElse_returnsDefaultWhenFailure() {
Result<String, Exception> result = Result.failure(new Exception("error"));
assertEquals("Should return default", "default", result.getOrElse("default"));
}
@Test
public void testWithIntegerType() {
Result<Integer, Exception> result = Result.success(42);
assertTrue("Should be success", result.isSuccess());
assertEquals(Integer.valueOf(42), result.getData());
}
@Test
public void testWithCustomException() {
class CustomException extends Exception {
public CustomException(String message) { super(message); }
}
Result<String, CustomException> result = Result.failure(new CustomException("custom error"));
assertTrue("Should be failure", result.isFailure());
assertEquals("custom error", result.getError().getMessage());
}
}
@@ -0,0 +1,204 @@
package com.example.boidelov3.game;
import com.example.boidelov3.Question;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Tests unitaires pour la classe GameEngine.
*/
public class GameEngineTest {
private GameEngine gameEngine;
private List<String> players;
@Before
public void setUp() {
gameEngine = new GameEngine();
players = Arrays.asList("Alice", "Bob", "Charlie", "David");
}
@Test
public void testSelectRandomPlayers_returnsCorrectCount() {
List<String> selected = gameEngine.selectRandomPlayers(players, 2);
assertEquals("Should select 2 players", 2, selected.size());
assertTrue("Should contain players from original list", players.containsAll(selected));
}
@Test
public void testSelectRandomPlayers_returnsUniquePlayers() {
List<String> selected = gameEngine.selectRandomPlayers(players, 3);
// Vérifier l'unicité
assertEquals("Should have 3 unique players", 3, new ArrayList<>(selected).size());
}
@Test
public void testSelectRandomPlayers_handlesCountLargerThanList() {
List<String> selected = gameEngine.selectRandomPlayers(players, 10);
assertEquals("Should select max available players", 4, selected.size());
}
@Test
public void testProcessQuestion_replacesJ1Placeholder() {
Question question = createQuestion("<J1> doit boire 2 gorgées");
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertFalse("Should not contain <J1> placeholder", text.contains("<J1>"));
assertTrue("Should contain a player name", containsAnyPlayer(text, players));
}
@Test
public void testProcessQuestion_replacesMultiplePlayerPlaceholders() {
Question question = createQuestion("<J1> et <J2> boivent ensemble");
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertFalse("Should not contain <J1>", text.contains("<J1>"));
assertFalse("Should not contain <J2>", text.contains("<J2>"));
}
@Test
public void testProcessQuestion_replacesVariante() {
Question question = createQuestion("C'est une question <variante> !");
question.setVariante(Arrays.asList("simple", "compliquée", "drôle"));
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertFalse("Should not contain <variante>", text.contains("<variante>"));
assertTrue("Should contain one of the variantes",
text.contains("simple") || text.contains("compliquée") || text.contains("drôle"));
}
@Test
public void testProcessQuestion_handlesDistribution() {
Question question = createQuestion("Question de test");
question.setDistribution(true);
question.setGorger(3);
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertTrue("Should contain 'distribue'", text.contains("distribue"));
assertTrue("Should contain gorgée count", text.contains("3"));
}
@Test
public void testProcessQuestion_handlesRecois() {
Question question = createQuestion("Question de test");
question.setRecois(true);
question.setGorger(2);
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertTrue("Should contain 'bois'", text.contains("bois"));
assertTrue("Should contain gorgée count", text.contains("2"));
}
@Test
public void testProcessQuestion_addsExtraGorgees() {
Question question = createQuestion("Question de test");
question.setDistribution(true);
question.setGorger(2);
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 3);
String text = processed.question.getQuestion();
assertTrue("Should contain total gorgée count (2+3=5)", text.contains("5"));
}
@Test
public void testProcessQuestion_handlesManches() {
Question question = createQuestion("Défi à manches <manches> !");
question.setArret("Arrêtez maintenant !");
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
assertTrue("Should be marked as manche", processed.isManche);
assertTrue("Should have active manche", gameEngine.hasActiveManche());
assertNotNull("Manche should have end message", processed.question.getArretMessageManche());
}
@Test
public void testUpdateManches_decrementsMancheCount() {
// Créer une manche
Question question = createQuestion("Défi <manches>");
question.setArret("Fin !");
gameEngine.processQuestion(question, players, 0);
// Récupérer le nombre initial
GameEngine.MancheState initialState = gameEngine.updateManches();
int initialCount = initialState.activeManche.getManchesRestantes();
// Mettre à jour
GameEngine.MancheState updatedState = gameEngine.updateManches();
assertEquals("Manche count should decrease", initialCount - 1, updatedState.activeManche.getManchesRestantes());
}
@Test
public void testClearManches_removesAllManches() {
// Ajouter des manches
Question question = createQuestion("Défi <manches>");
question.setArret("Fin !");
gameEngine.processQuestion(question, players, 0);
gameEngine.clearManches();
assertFalse("Should have no active manches", gameEngine.hasActiveManche());
assertEquals("Should have 0 active manches", 0, gameEngine.getActiveManchesCount());
}
@Test
public void testProcessQuestion_handlesPluralGorgees() {
Question question = createQuestion("Test");
question.setDistribution(true);
question.setGorger(2);
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertTrue("Should use plural 'gorgées'", text.contains("gorgées"));
}
@Test
public void testProcessQuestion_handlesSingularGorgee() {
Question question = createQuestion("Test");
question.setDistribution(true);
question.setGorger(1);
GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0);
String text = processed.question.getQuestion();
assertTrue("Should use singular 'gorgée'", text.contains("gorgée"));
}
// Helper methods
private Question createQuestion(String text) {
Question q = new Question();
q.setId(1);
q.setQuestion(text);
return q;
}
private boolean containsAnyPlayer(String text, List<String> players) {
for (String player : players) {
if (text.contains(player)) {
return true;
}
}
return false;
}
}