diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0000157..4f618e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ if (localPropertiesFile.exists()) { android { namespace 'com.example.boidelov3' - compileSdk 33 + compileSdk 35 buildFeatures { buildConfig = true } @@ -19,16 +19,18 @@ android { defaultConfig { applicationId "com.example.boidelov3" minSdk 24 - targetSdk 33 + targetSdk 35 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - // Inject database credentials into BuildConfig - buildConfigField "String", "DB_URL", "\"${localProperties.getProperty('db.url', '')}\"" - buildConfigField "String", "DB_USER", "\"${localProperties.getProperty('db.user', '')}\"" - buildConfigField "String", "DB_PASSWORD", "\"${localProperties.getProperty('db.password', '')}\"" + // IMPORTANT: Database credentials should NEVER be stored in BuildConfig + // Use a secure backend API instead, or Android Keystore for local storage + // These fields are kept empty for backward compatibility but will be removed + buildConfigField "String", "DB_URL", "\"\"" + buildConfigField "String", "DB_USER", "\"\"" + buildConfigField "String", "DB_PASSWORD", "\"\"" } buildTypes { @@ -45,14 +47,14 @@ android { dependencies { - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - implementation 'com.squareup.okhttp3:okhttp:4.9.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.3' - implementation 'com.google.code.gson:gson:2.8.8' + implementation 'com.google.code.gson:gson:2.11.0' } \ No newline at end of file diff --git a/app/src/main/assets/question.json b/app/src/main/assets/question.json index 47b161f..4045554 100644 --- a/app/src/main/assets/question.json +++ b/app/src/main/assets/question.json @@ -1,4 +1,3 @@ - { "version": "2", "questions": [ @@ -8,7 +7,6 @@ "gorger": 3, "distribution": true, "recois": true - }, { "id": 2, @@ -39,140 +37,172 @@ "recois": true }, { - "id": 5, + "id": 6, "question": "La dernière personne à avoir vomi en soirée ", "gorger": 3, "distribution": true, "recois": true }, { - "id": 6, + "id": 7, "question": "Toutes celles (ou ceux) qui ont du verni à ongles", "gorger": 3, "distribution": true, "recois": true }, { - "id": 7, + "id": 8, "question": "Tous les joueurs célibataires", "gorger": 3, "distribution": true, "recois": true }, { - "id": 8, + "id": 9, "question": "Tous ceux qui ont des lunettes", "gorger": 2, "distribution": true, "recois": true }, { - "id": 9, + "id": 10, "question": "Tous ceux qui ont déjà triché à un examen", "gorger": 3, "distribution": false, "recois": true }, { - "id": 10, + "id": 11, "question": "Le/La plus radin(e)", "gorger": 4, "distribution": false, "recois": true }, { - "id": 11, + "id": 12, "question": " à toi de juger : entre et qui stresse le plus pour un rien selon toi? Le perdant ", "gorger": 5, "distribution": false, "recois": true - }, { - "id": 12, + }, + { + "id": 13, "question": "Le mec qui a le plus gros ventre à bière ", "gorger": 3, "distribution": true, "recois": true }, { - "id": 13, + "id": 14, "question": "Tous ceux qui se sont déjà fait exclure de cours", "gorger": 3, "distribution": false, "recois": true }, { - "id": 14, + "id": 15, "question": ", donne le nombre d'habitant du/de la ( à 1 000 000 près) Si tu as faux tu bois, sinon tu ", "gorger": 3, "distribution": true, "recois": false, - "variante": ["Tadjikistant", "Monaco", "Belgique", "Suisse", "Allemagne", "Chine", "Inde"] + "variante": [ + "Tadjikistant", + "Monaco", + "Belgique", + "Suisse", + "Allemagne", + "Chine", + "Inde" + ] }, { - "id": 15, + "id": 16, "question": "Tous ceux qui ont des frères et soeurs", "gorger": 3, "distribution": false, "recois": true }, - { - "id": 16, - "question": " à qui appartient le slogan suivant? ", - "variante": ["Des pâtes oui mais des (panzani)"," Le plaisir pour les petites faims. (Kinder Bueno)","Réveillez le lion qui est en vous. (Cereale lion)","Parce que le monde bouge (CIC)","Ça coule de source ! (Cristaline)", "Pensez différemment (apple)" ], - "gorger": 3, - "distribution": true, - "recois": true - }, { "id": 17, - "question": "Celles et ceux qui ont eu un Windows phone", + "question": " à qui appartient le slogan suivant? ", + "variante": [ + "Des pâtes oui mais des (panzani)", + " Le plaisir pour les petites faims. (Kinder Bueno)", + "Réveillez le lion qui est en vous. (Cereale lion)", + "Parce que le monde bouge (CIC)", + "Ça coule de source ! (Cristaline)", + "Pensez différemment (apple)" + ], "gorger": 3, "distribution": true, "recois": true }, { "id": 18, + "question": "Celles et ceux qui ont eu un Windows phone", + "gorger": 3, + "distribution": true, + "recois": true + }, + { + "id": 19, "question": ", Boire un café fait baisser le taux d'alcool? Si tu as faux ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 19, + "id": 20, "question": "Celles/Ceux qui se sont déjà battus", "gorger": 3, "distribution": false, "recois": true }, { - "id": 20, + "id": 21, "question": "Celui/Celle qui pèse le plus lourd", - "variante": ["lourd","leger"], + "variante": [ + "lourd", + "leger" + ], "gorger": 3, "distribution": true, "recois": false }, { - "id": 21, + "id": 22, "question": "Pour se décoincer, le/la plus ", - "variante": ["timide", "enervé ", "angoissé","aigri"], + "variante": [ + "timide", + "enervé ", + "angoissé", + "aigri" + ], "gorger": 2, "distribution": false, "recois": true }, { - "id": 22, + "id": 23, "question": "Le/La plus ", "gorger": 3, - "variante": ["jeune","vieille"], + "variante": [ + "jeune", + "vieille" + ], "distribution": false, "recois": true }, { - "id": 23, + "id": 24, "question": "Celles/Ceux qui ont fait des études ", "gorger": 3, - "variante": ["L","Scientifique", "Bac Professionel", "STMG"], + "variante": [ + "L", + "Scientifique", + "Bac Professionel", + "STMG" + ], "distribution": true, "recois": true }, @@ -180,7 +210,12 @@ "id": 25, "question": "Celles et ceux qui ont deja joué de maniere recurrente ", "gorger": 3, - "variante": ["du piano","de la guitare", "du saxophone", "de la batterie"], + "variante": [ + "du piano", + "de la guitare", + "du saxophone", + "de la batterie" + ], "distribution": true, "recois": true }, @@ -217,7 +252,10 @@ "id": 30, "question": "Celui/Celle qui a les cheveux les plus ", "gorger": 2, - "variante": ["longs","courts"], + "variante": [ + "longs", + "courts" + ], "distribution": true, "recois": true }, @@ -234,7 +272,8 @@ "gorger": 2, "distribution": true, "recois": true - }, { + }, + { "id": 33, "question": "Celles/Ceux qui ont déjà appelé leur partenaire par le prénom de leurs ex", "gorger": 4, @@ -247,7 +286,12 @@ "gorger": 3, "distribution": true, "recois": true, - "variante": ["vodka", "rhum", "tequila", "whisky"] + "variante": [ + "vodka", + "rhum", + "tequila", + "whisky" + ] }, { "id": 35, @@ -255,7 +299,14 @@ "gorger": 3, "distribution": false, "recois": true, - "variante": ["a","e","i","o","u","y"] + "variante": [ + "a", + "e", + "i", + "o", + "u", + "y" + ] }, { "id": 36, @@ -270,7 +321,14 @@ "gorger": 3, "distribution": false, "recois": true, - "variante": ["21","22","23","24","25","26"] + "variante": [ + "21", + "22", + "23", + "24", + "25", + "26" + ] }, { "id": 38, @@ -293,7 +351,14 @@ "gorger": 1, "distribution": true, "recois": true, - "variante": [0,1,2,3,4,5] + "variante": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] }, { "id": 41, @@ -326,28 +391,33 @@ "recois": true }, { - "id": 30, + "id": 45, "question": "Celui/Celle avec le plus de follow sur ", "gorger": 3, "distribution": true, "recois": true, - "variante": ["instagram","facebook","tik-tok"] + "variante": [ + "instagram", + "facebook", + "tik-tok" + ] }, { - "id": 31, + "id": 46, "question": "Celui/Celle avec le nom de famille le plus compliqué", "gorger": 3, "distribution": true, "recois": true }, { - "id": 32, + "id": 47, "question": "Les joueurs qui n'ont pas encore distribué de gorgées", "gorger": 3, "distribution": false, "recois": true - }, { - "id": 33, + }, + { + "id": 48, "question": "Le premier joueur à donner l'heure", "doubledistribution": true, "gorger": 3, @@ -355,80 +425,89 @@ "recois": true }, { - "id": 34, + "id": 49, "question": "Celles et ceux qui ont déjà dépenser plus de 2000 euros en un achat", "gorger": 3, "distribution": true, "recois": true }, { - "id": 35, + "id": 50, "question": "La personne la moins courageuse", "gorger": 1, "distribution": false, "recois": true }, { - "id": 36, + "id": 51, "question": "Celles/Ceux qui rentre chez eux à la fin de la soirée", "gorger": 3, "distribution": true, "recois": true }, { - "id": 37, + "id": 52, "question": "Celles et ceux dont le jour d'anniversaire est un nombre ", "gorger": 3, "distribution": true, "recois": true, - "variante": ["impaire","paire"] + "variante": [ + "impaire", + "paire" + ] }, { - "id": 38, + "id": 53, "question": "Si tu as un téléphone qui a un prix supérieur d'achat à 1000 euros, tu es riche donc en tant que personne généreuse", "gorger": 3, "distribution": true, "recois": false }, { - "id": 39, + "id": 54, "question": "Tous ceux qui ont fait plus de 100km pour venir ici ", "gorger": 3, "distribution": true, "recois": false }, { - "id": 40, + "id": 55, "jeux": true, "question": "Le jeu du revolver: Chaque joueur a maintenant 7 balles, une balle correspondant à une gorgée. Les joueurs ont la capacité de tirer des balles à tout moment du jeu.", "distribution": false, "recois": false }, { - "id": 41, + "id": 56, "question": "", "gorger": 3, "distribution": true, "recois": true }, { - "id": 42, + "id": 57, "jeux": true, "question": " est le vieu/vieille briscard ! Interdiction de montrer tes dents pendant manches", "manches": true, "arret": "Tu peux arreter de cacher tes belles dents" - }, { - "id": 43, + "id": 58, "jeux": true, "question": " et liser le premier SMS qui s'affiche quand on tape dans la barre de recherche. le plus marrant", "gorger": 3, - "variante": ["désolé","caca","bourré","mine","nazi"], + "variante": [ + "désolé", + "caca", + "bourré", + "mine", + "nazi" + ], "distribution": true, "recois": false - }, { - "id": 44, + }, + { + "id": 59, "jeux": true, "question": "A tour de role, vous avez exactement 3 secondes pour donner un mot en rapport avec le mots dit precedemment. tu commences en choissisant un mot. Le perdant", "gorger": 3, @@ -436,7 +515,7 @@ "recois": true }, { - "id": 45, + "id": 60, "jeux": true, "question": "Le jeu du PIM PAM POUM. Le premier qui perd ", "gorger": 3, @@ -444,7 +523,7 @@ "recois": true }, { - "id": 46, + "id": 61, "jeux": true, "question": " defie au chifoumi ! Le perdant", "gorger": 3, @@ -452,10 +531,13 @@ "recois": true }, { - "id": 47, + "id": 62, "jeux": true, "question": " est dans le ! Tu dois parler au pendant manches ", - "variante": ["passé", "futur"], + "variante": [ + "passé", + "futur" + ], "manches": true, "arret": "Tu est revenu dans le present", "distribution": false, @@ -463,11 +545,11 @@ "caliente": false }, { - "id": 48, + "id": 63, "question": " tu bois autant de gorgées que tu as d'années d'études après le BAC" }, { - "id": 49, + "id": 64, "jeux": true, "question": " et ferment leurs yeux ! Ils/Elles doivent deviner la couleur des yeux de l'autre. ", "gorger": 3, @@ -475,7 +557,7 @@ "recois": true }, { - "id": 50, + "id": 65, "jeux": true, "question": " est manchot ! Il/Elle ne peut plus utiliser ses doigts durant manches . Si il/elle s'en sert, il/elle devra boire autant de gorgées qu'il/elle a utilisé de doigts", "distribution": false, @@ -483,21 +565,19 @@ "recois": true }, { - "id": 51, + "id": 66, "jeux": true, "question": " et , si vous êtes ensemble dans la vraie vie, vous pouvez distribuer 2 gorgées , autrement buvez-les" - }, { - "id": 52, + "id": 67, "jeux": true, "question": " a la tourette ! A chaque fois que tu bois une gorgée, tu dois CRIER une insulte. C'est un stade avancé, ça dure manches", "manches": true, "arret": "Tu n'as plus tourette." - }, { - "id": 53, + "id": 68, "jeux": true, "question": ", donne la couleur préférée de si tu te trompes", "gorger": 3, @@ -505,29 +585,37 @@ "recois": true }, { - "id": 54, + "id": 69, "jeux": true, "question": " à l'oeil de serpent ! Dès qu'un joueur te regarde dans les yeux, il/elle boit. Si tu es arrivé à faire boire personne avant manches, tu ", "gorger": 4, "distribution": false, "recois": true - }, { - "id": 55, + }, + { + "id": 70, "jeux": true, "question": " et se mesurent ! Le/la plus ", "gorger": 3, - "variante": ["petit","grand"], + "variante": [ + "petit", + "grand" + ], "distribution": true, "recois": true }, { - "id": 56, + "id": 71, "jeux": true, "question": " doit terminer toutes ses phrases par - pendant manches", - "variante": ["C'est clair","han","quoicoubeh"] + "variante": [ + "C'est clair", + "han", + "quoicoubeh" + ] }, { - "id": 57, + "id": 72, "jeux": true, "question": " et se défient au 'je te tiens, tu me tiens', le premier qui rit sera dechiré, et ", "gorger": 3, @@ -535,23 +623,27 @@ "recois": true }, { - "id": 58, + "id": 73, "jeux": true, "question": " et n'ont plus le droit d'utiliser leur téléphone pendant manches !", "manches": "true", - "arret": "vous pouvez enfin utiliser vos téléphone bande de geek" + "arret": "vous pouvez enfin utiliser vos téléphone bande de geek" }, { - "id": 59, + "id": 74, "jeux": true, "question": " et racontent une anecdote, celui/celle qui sort la plus ", - "variante": ["banale","incroyable","marrante"], + "variante": [ + "banale", + "incroyable", + "marrante" + ], "gorger": 3, "distribution": true, "recois": true }, { - "id": 60, + "id": 75, "jeux": true, "question": ", Si on te dit Marco? ... Si tu as dis Polo ", "gorger": 3, @@ -559,7 +651,7 @@ "recois": true }, { - "id": 61, + "id": 76, "jeux": true, "question": " est l'aigris pendant manches ! Dès que tu souris ou rigoles,", "gorger": 1, @@ -569,7 +661,7 @@ "recois": true }, { - "id": 62, + "id": 77, "jeux": true, "question": " fait un geste, le suivant répète et en ajoute un. Le perdant ", "gorger": 3, @@ -577,17 +669,17 @@ "recois": true }, { - "id": 63, + "id": 78, "question": ", et vont désigner quelqu'un qui doit terminer son verre", "jeux": true }, { - "id": 64, + "id": 79, "question": "Récitez l'alphabet en énonçant une lettre à tour de rôle. Si finit son verre avant, cul sec pour tout le monde !", "jeux": true }, { - "id": 65, + "id": 80, "question": "Si arrive à finir son verre en moins de 5 secondes, il/elle ", "gorger": 6, "distribution": true, @@ -595,34 +687,34 @@ "jeux": true }, { - "id": 66, + "id": 81, "jeux": true, "question": " et sont lies, si l'un boit alors l'autre aussi, et ce pendant manches", "manches": true, - "arret": "Vous n'etes plus liés par le verre de l'amitié et plus.." + "arret": "Vous n'etes plus liés par le verre de l'amitié et plus.." }, { - "id": 67, + "id": 82, "question": " dit un mot, la personne suivante le répète et en ajoute un nouveau, ainsi de suite jusqu'à ce que quelqu'un se trompe. Le perdant boit autant de gorgées qu'il y a eu de personne avant lui", "jeux": true }, { - "id": 68, + "id": 83, "question": " doit choisir un mot que tout le monde devra dire à chaque fois qu'une personne boit. Pendant manches", "jeux": false, "manches": true, "arret": "Plus besoin de dire le mots avant de boire" }, { - "id": 69, + "id": 84, "question": "Quand l'heure affichera un multiple de 10 (22h, 22h10 ...) le premier à crier 'merde j'ai oublié mon chat'", "gorger": 3, - "distribut":false, + "distribut": false, "manches": true, "arret": "Vous avez retrouver le chat!" }, { - "id": 70, + "id": 85, "question": "Plutôt ne plus avoir de mains ou de jambes? les perdants ", "gorger": 3, "jeux": false, @@ -630,29 +722,37 @@ "recois": true }, { - "id": 71, + "id": 86, "question": "Vive la poésie ! Nos phrases doivent rimer sous peine d'une gorgée pendant manches", "manches": true, "arret": "Vous ne devez plus faire de rime." }, { - "id": 72, + "id": 87, "question": "Choisissez le joueur le drôle d'entre vous, ce dernier ", - "variante": ["moins", "plus"], + "variante": [ + "moins", + "plus" + ], "gorger": 3, "distribution": false, "recois": true }, { - "id": 73, + "id": 88, "question": "Les ", - "variante": ["filles","garçons","couples","celibataires"], + "variante": [ + "filles", + "garçons", + "couples", + "celibataires" + ], "gorger": 3, "distribution": true, "recois": true }, { - "id": 74, + "id": 89, "jeux": true, "question": "Plutôt avoir un tapis volant, ou un frigo qui se remplit tout seul ? Votez tous en même temps. La minorité ", "gorger": 3, @@ -661,15 +761,14 @@ "manches": false }, { - "id": 75, + "id": 90, "question": "Plutôt avoir du temps ou de l'argent ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true - }, { - "id": 76, + "id": 91, "question": "Le premier joueur qui en embrasse un autre sur la bouche", "gorger": 5, "distribution": true, @@ -677,82 +776,103 @@ "caliente": true }, { - "id": 77, + "id": 92, "question": "Plutôt série ou film ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 78, + "id": 93, "question": "Plutôt jeux-videos ou livre ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, - { - "id": 79, + "id": 94, "question": "Plutôt anime ou jeux-videos ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 80, + "id": 95, "question": "Elisez le plus d'entre vous, ce dernier ", - "variante": ["con", "intelligent", "beau", "gentil","dragueur","timide"], + "variante": [ + "con", + "intelligent", + "beau", + "gentil", + "dragueur", + "timide" + ], "gorger": 3, "distribution": true, "recois": true }, { - "id": 81, + "id": 96, "question": "Le premier qui donne un film de ", "gorger": 3, "distribution": true, "recois": false, - "variante": ["Christopher Nolan","James Cameron","Tim Burton","Quentin tarantino","Steven Spielberg"] + "variante": [ + "Christopher Nolan", + "James Cameron", + "Tim Burton", + "Quentin tarantino", + "Steven Spielberg" + ] }, { - "id": 82, + "id": 97, "question": "Le premier qui donne un film avec ", - "variante": ["Christian Clavier","Morgan freeman","Brad Pitt", "Jean Reno","Marion Cotillard"], + "variante": [ + "Christian Clavier", + "Morgan freeman", + "Brad Pitt", + "Jean Reno", + "Marion Cotillard" + ], "gorger": 3, "distribution": true, "recois": false }, { - "id": 83, + "id": 98, "question": "La première personne qui désigne le/la plus ", "gorger": 3, "distribution": true, "recois": false, - "variante": ["jeune", "vieille"] + "variante": [ + "jeune", + "vieille" + ] }, { - "id": 84, + "id": 99, "question": "Plutôt avoir des connaissances illimitées ou diriger le monde ? Votez tous en même temps. La minorité", "gorger": 3, "distribution": false, "recois": true }, { - "id": 85, + "id": 100, "question": "Plutôt n'avoir aucun ami ou ne plus pouvoir utiliser d'appareil électronique ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 86, + "id": 101, "question": "Plutot vaincre le patrikaka ou la polution dans le monde? Votez tous en meme temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 87, + "id": 102, "question": "Jeu du LUTIN : Pendant manches. Vous devez enlever le lutin de votre verre pour pouvoir boire et le remettre ensuite sinon vous devait reboire", "gorger": 3, "distribution": false, @@ -761,428 +881,462 @@ "arret": "Le Lutin est parti. Vous pouvez boire normalement" }, { - "id": 88, + "id": 103, "question": "Le premier joueur à ramener un objet (pas de vêtements) ", "gorger": 3, "distribution": true, - "variante": ["rouge","vert","bleu","jaune"], + "variante": [ + "rouge", + "vert", + "bleu", + "jaune" + ], "recois": false, "manches": false }, { - "id": 89, + "id": 104, "question": "Le premier joueur qui dévoile un de ses secrets et que personne ne sait ", "gorger": 3, "distribution": true, "recois": false }, { - "id": 90, + "id": 105, "question": "Jeu des peaux! Triez vous du joueur le plus bronzé au joueur le moins bronzé. Le plus bronzé prend 1 gorgée, le second 2 gorgées, etc.", "gorger": 3, "distribution": true, "recois": true }, { - "id": 91, + "id": 106, "question": "Plutôt avoir du pouvoir ou de la connaissance ? Votez tous en même temps. La minorité ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 92, + "id": 107, "question": "Il est désormais interdit de se tutoyer pendant manches", "manches": true, - "arret":"Vous pouvez vous enfin vous tutoyer" - + "arret": "Vous pouvez vous enfin vous tutoyer" }, { - "id": 93, + "id": 108, "question": "Vaccin contre le COVID19 : Le DR Raoul à dit que boire un cul sec avait les mêmes effets que le vaccin. Tout le monde bois sont verre cul sec" }, { - "id": 94, - "question": "Faites un concours de clash : Rap contenders entre et le perdant ", + "id": 109, + "question": "Faites un concours de clash : Rap contenders entre et le perdant ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 95, + "id": 110, "question": "Choisissez un joueur qui doit imiter un animal pendant manches sans se faire prendre", "manches": true, "arret": "Fin de l'imitation animale" }, { - "id": 96, + "id": 111, "question": "Qui a le plus de talents cachés ? Votez et le perdant", "gorger": 2, "distribution": false, "recois": true }, { - "id": 97, + "id": 112, "question": "Le prochain qui parle de travail ou d'école doit boire un shot", "duree": true, "arret": "Fin de l'interdiction" }, { - "id": 98, + "id": 113, "question": "Karaoké improvisé : choisit une chanson pour ", "gorger": 3, "distribution": false, "recois": true }, { - "id": 99, + "id": 114, "question": "Qui serait le plus susceptible de survivre dans un film de survie ? Votez", "gorger": 3, "distribution": false, "recois": true }, { - "id": 100, + "id": 115, "question": "Défi cuisine : doit créer la combinaison d'alcool la plus bizarre avec les ingrédients présents, doit boire la mixture", "gorger": 2, "distribution": false, "recois": true }, { - "id": 101, + "id": 116, "question": "Ceux qui ont déjà fait du ski", "gorger": 2, "distribution": true, "recois": true }, { - "id": 102, + "id": 117, "question": "Ceux qui ont un tatouage", "gorger": 2, "distribution": true, "recois": true }, { - "id": 103, + "id": 118, "question": "Ceux qui portent des lentilles de contact", "gorger": 2, "distribution": true, "recois": true }, { - "id": 104, + "id": 119, "question": "Ceux qui ont déjà fait un crochet en blanc", "gorger": 3, "distribution": false, "recois": true }, { - "id": 105, + "id": 120, "question": "Ceux qui ont plus de 1000 abonnés sur Instagram", "gorger": 3, "distribution": true, "recois": true }, { - "id": 106, + "id": 121, "question": "Ceux qui parlent une autre langue que le français", "gorger": 2, "distribution": true, "recois": true }, { - "id": 107, + "id": 122, "question": "Ceux qui ont déjà été en couple avec quelqu'un dans la pièce", "gorger": 4, "distribution": false, "recois": true }, { - "id": 108, + "id": 123, "question": "Les joueurs qui ont un iPhone", "gorger": 2, "distribution": true, "recois": true }, { - "id": 109, + "id": 124, "question": "Ceux qui ont déjà participé à un jeu télévisé", "gorger": 3, "distribution": true, "recois": true }, { - "id": 110, + "id": 125, "question": "Ceux qui ont déjà fait une manif", "gorger": 2, "distribution": true, "recois": true }, { - "id": 111, + "id": 126, "question": "Le/La plus susceptible du groupe", "gorger": 3, "distribution": false, "recois": true }, { - "id": 112, + "id": 127, "question": "Le/La plus drôle du groupe (vote à main levée, le perdant boit)", "gorger": 3, "distribution": false, "recois": true }, { - "id": 113, + "id": 128, "question": "Le/La plus 'bête' cul du groupe", "gorger": 4, "distribution": false, "recois": true }, { - "id": 114, + "id": 129, "question": "Le/La plus timide", "gorger": 2, "distribution": false, "recois": true }, { - "id": 115, + "id": 130, "question": "Le/La plus likely to rater sa vie", "gorger": 4, "distribution": false, "recois": true }, { - "id": 116, + "id": 131, "question": "Celui/Celle qui a le plus de stories Instagram", "gorger": 2, "distribution": true, "recois": true }, { - "id": 117, + "id": 132, "question": "Les célibataires de plus de ans", "gorger": 4, "distribution": true, "recois": true, - "variante": ["23","25","27","30"] + "variante": [ + "23", + "25", + "27", + "30" + ] }, { - "id": 118, + "id": 133, "question": "Ceux qui ont déjà ghosté quelqu'un", "gorger": 3, "distribution": false, "recois": true }, { - "id": 119, + "id": 134, "question": "Celui/Celle qui a fait le pire premier baiser de l'histoire", "gorger": 4, "distribution": false, "recois": true }, { - "id": 120, + "id": 135, "question": "Le/La plus 'pick up artist' du groupe", "gorger": 3, "distribution": false, "recois": true }, { - "id": 121, + "id": 136, "question": " doit parler avec un accent jusqu'à la fin du défi. manches", "gorger": 0, - "variante": ["belge","suisse","canadien","allemand","anglais","espagnol"], + "variante": [ + "belge", + "suisse", + "canadien", + "allemand", + "anglais", + "espagnol" + ], "arret": "Accent oublié!" }, { - "id": 122, + "id": 137, "question": "Interdiction de dire 'oui' ou 'non' pendant manches. Si tu dis oui/non : 3 gorgées !", "gorger": 0, "arret": "Plus d'interdiction !" }, { - "id": 123, + "id": 138, "question": " doit inventer un surnom ridicule à et l'utiliser pendant manches", "gorger": 0, "arret": "Surnom oublié!" }, { - "id": 124, + "id": 139, "question": "Pause pip interdite pendant manches. 5 gorgées si tu vas aux toilettes !", "gorger": 0, "arret": "Tu peux enfin aller aux toilettes !" }, { - "id": 125, + "id": 140, "question": " ne doit plus répondre quand on l'appelle par son prénom pendant manches. 3 gorgées par oubli !", "gorger": 0, "arret": "Tu peux enfin répondre à ton prénom !" }, { - "id": 126, + "id": 141, "question": "Mini-quiz géographique : doit nommer la capitale d'un pays. Si faux : 2 gorgées. Continue pendant manches ou jusqu'à 3 bonnes réponses.", "gorger": 0, "arret": "Quiz terminé !" }, { - "id": 127, + "id": 142, "question": " doit compléter toutes les phrases des autres par 'coucou bigourd' pendant manches", "gorger": 0, "arret": "Plus de coucou bigourd !" }, { - "id": 128, + "id": 143, "question": " est le serviteur personnel de pendant manches. peut demander des gorgées !", "gorger": 0, "arret": "Servitude terminée !" }, { - "id": 129, + "id": 144, "question": "Interdiction de rire pendant manches. 4 gorgées si tu ris !", "gorger": 0, "arret": "Tu peux enfin rire !" }, { - "id": 130, + "id": 145, "question": " doit faire une imitation d'un animal au choix de toutes les 3 questions pendant manches", "gorger": 0, "arret": "Plus d'imitations !" }, { - "id": 131, + "id": 146, "question": " et se regardent dans les yeux pendant 10 secondes sans rire. Le premier à rire boit 4 gorgées", "gorger": 0, "recois": true }, { - "id": 132, + "id": 147, "question": " doit donner un compliment à . Si n'est pas satisfait : boit 3 gorgées", "gorger": 0, "recois": true }, { - "id": 133, + "id": 148, "question": " à toi de choisir : qui de ou a le plus mauvais goût musical ? Le perdant boit 3 gorgées", "gorger": 0, "recois": true }, { - "id": 134, + "id": 149, "question": " doit deviner ce que a mangé aujourd'hui. 1 gorgée par mauvaise réponse, max 5", "gorger": 0, "recois": true }, { - "id": 135, + "id": 150, "question": " et font un bras de fer. Le perdant boit 5 gorgées", "gorger": 0, "recois": true }, { - "id": 136, + "id": 151, "question": " doit nommer une qualité de . Si n'est pas d'accord : 3 gorgées pour ", "gorger": 0, "recois": true }, { - "id": 137, + "id": 152, "question": " doit deviner la couleur des sous-vêtements de . 2 gorgées si faux", "gorger": 0, "recois": true }, { - "id": 138, + "id": 153, "question": " doit mimer le métier de . Les autres doivent deviner en moins de 30 secondes ou boit 4 gorgées", "gorger": 0, "recois": true }, { - "id": 139, + "id": 154, "question": "Ceux qui ont déjà fait du ", "gorger": 2, "distribution": true, "recois": true, - "variante": ["cheval","velo","kayak","surf","paintball"] + "variante": [ + "cheval", + "velo", + "kayak", + "surf", + "paintball" + ] }, { - "id": 140, + "id": 155, "question": "Les fans de ", "gorger": 3, "distribution": true, "recois": true, - "variante": ["Harry Potter","Star Wars","Marvel","Game of Thrones","One Piece"] + "variante": [ + "Harry Potter", + "Star Wars", + "Marvel", + "Game of Thrones", + "One Piece" + ] }, { - "id": 141, + "id": 156, "question": "Ceux qui écoutent du ", "gorger": 2, "distribution": true, "recois": true, - "variante": ["rap","rock","electro","metal","jazz","classique"] + "variante": [ + "rap", + "rock", + "electro", + "metal", + "jazz", + "classique" + ] }, { - "id": 142, + "id": 157, "question": "Ceux qui ont déjà fait un binge watch d'une série en moins de 24h", "gorger": 2, "distribution": true, "recois": true }, { - "id": 143, + "id": 158, "question": "Le/La plus 'influençant' du groupe (vote à main levée)", "gorger": 3, "distribution": false, "recois": true }, { - "id": 144, + "id": 159, "question": "Ceux qui ont un animal de compagnie", "gorger": 1, "distribution": true, "recois": true }, { - "id": 145, + "id": 160, "question": " doit dire 3 mots commençant par la même lettre que choisit. Si échec : 3 gorgées", "gorger": 0, "recois": true }, { - "id": 146, + "id": 161, "question": "Ceux qui ont déjà dormi dehors (camping, festival, etc.)", "gorger": 2, "distribution": true, "recois": true }, { - "id": 147, + "id": 162, "question": "Le/La plus 'branché' du groupe selon les autres", "gorger": 3, "distribution": false, "recois": true }, { - "id": 148, + "id": 163, "question": "Ceux qui ont un_permis de conduire", "gorger": 1, "distribution": true, "recois": true }, { - "id": 149, + "id": 164, "question": " doit répéter tout ce que dit comme un perroquet pendant manches", "gorger": 0, "arret": "Plus de perroquet !" }, { - "id": 150, + "id": 165, "question": "Ceux qui ont déjà fait un saut à l'élastique ou en parachute", "gorger": 3, "distribution": true, "recois": true } - ] -} +} \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..49e04e4 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/boidelov3/DatabaseConnection.java b/app/src/main/java/com/example/boidelov3/DatabaseConnection.java index 04e8e6a..bd67059 100644 --- a/app/src/main/java/com/example/boidelov3/DatabaseConnection.java +++ b/app/src/main/java/com/example/boidelov3/DatabaseConnection.java @@ -14,12 +14,23 @@ 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. + * + * SECURITY WARNING: This class is currently disabled because database credentials + * should NEVER be stored in BuildConfig or in the app code. + * + * Recommended secure alternatives: + * 1. Use a backend API that handles database connections + * 2. Use Android Keystore for storing encrypted credentials locally + * 3. Use Firebase Authentication or similar secure services + * + * To re-enable database functionality, implement one of the secure solutions above. */ public class DatabaseConnection { - 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; + // Database credentials are now disabled for security + private static final String DB_URL = ""; + private static final String USER = ""; + private static final String PASSWORD = ""; private final ExecutorService executorService; private final Handler mainHandler; diff --git a/app/src/main/java/com/example/boidelov3/EndGameActivity.java b/app/src/main/java/com/example/boidelov3/EndGameActivity.java index e2a5827..927bbb0 100644 --- a/app/src/main/java/com/example/boidelov3/EndGameActivity.java +++ b/app/src/main/java/com/example/boidelov3/EndGameActivity.java @@ -10,6 +10,7 @@ import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import com.example.boidelov3.data.PlayerStats; import com.google.android.material.button.MaterialButton; import java.util.ArrayList; @@ -24,6 +25,8 @@ public class EndGameActivity extends AppCompatActivity { private TextView questionsPlayedValue; private TextView playersCountValue; private TextView gorgeesTotalValue; + private TextView plusBuValue; + private TextView plusDistribueValue; private MaterialButton homeButton; private MaterialButton replayButton; @@ -31,6 +34,7 @@ public class EndGameActivity extends AppCompatActivity { private int questionsPlayed; private int playersCount; private ArrayList players; + private ArrayList playerStatsList; @Override protected void onCreate(Bundle savedInstanceState) { @@ -60,6 +64,8 @@ public class EndGameActivity extends AppCompatActivity { questionsPlayedValue = findViewById(R.id.questionsPlayedValue); playersCountValue = findViewById(R.id.playersCountValue); gorgeesTotalValue = findViewById(R.id.gorgeesTotalValue); + plusBuValue = findViewById(R.id.plusBuValue); + plusDistribueValue = findViewById(R.id.plusDistribueValue); homeButton = findViewById(R.id.homeButton); replayButton = findViewById(R.id.replayButton); @@ -76,6 +82,7 @@ public class EndGameActivity extends AppCompatActivity { questionsPlayed = getIntent().getIntExtra("EXTRA_QUESTIONS_PLAYED", 0); playersCount = getIntent().getIntExtra("EXTRA_PLAYERS_COUNT", 0); players = getIntent().getStringArrayListExtra("EXTRA_PLAYERS"); + playerStatsList = getIntent().getParcelableArrayListExtra("EXTRA_PLAYER_STATS"); // Si pas de données, utiliser les SharedPreferences if (questionsPlayed == 0) { @@ -93,13 +100,53 @@ public class EndGameActivity extends AppCompatActivity { animateValue(questionsPlayedValue, 0, questionsPlayed, 1000); animateValue(playersCountValue, 0, playersCount, 1000); - // Afficher les joueurs (simplifié pour l'instant) - if (players != null && !players.isEmpty()) { - StringBuilder playersText = new StringBuilder(); - for (int i = 0; i < players.size(); i++) { - if (i > 0) playersText.append(", "); - playersText.append(players.get(i)); + // Calculer et afficher les statistiques détaillées + if (playerStatsList != null && !playerStatsList.isEmpty()) { + // Calculer le total des gorgées bues + int totalGorgeesBuves = 0; + PlayerStats biggestDrinker = null; + int maxGorgeesBuves = -1; + + PlayerStats biggestDistributor = null; + int maxGorgeesDistribuees = -1; + + for (PlayerStats stats : playerStatsList) { + totalGorgeesBuves += stats.getGorgeesBuves(); + + // Trouver le plus gros buveur + if (stats.getGorgeesBuves() > maxGorgeesBuves) { + maxGorgeesBuves = stats.getGorgeesBuves(); + biggestDrinker = stats; + } + + // Trouver le plus gros distributeur + if (stats.getGorgeesDistribuees() > maxGorgeesDistribuees) { + maxGorgeesDistribuees = stats.getGorgeesDistribuees(); + biggestDistributor = stats; + } } + + // Afficher le total des gorgées bues + animateValue(gorgeesTotalValue, 0, totalGorgeesBuves, 1000); + + // Afficher le joueur qui a le plus bu + if (biggestDrinker != null && biggestDrinker.getGorgeesBuves() > 0) { + plusBuValue.setText(biggestDrinker.getPlayerName() + " (" + biggestDrinker.getGorgeesBuves() + ")"); + } else { + plusBuValue.setText("Personne"); + } + + // Afficher le joueur qui a le plus distribué + if (biggestDistributor != null && biggestDistributor.getGorgeesDistribuees() > 0) { + plusDistribueValue.setText(biggestDistributor.getPlayerName() + " (" + biggestDistributor.getGorgeesDistribuees() + ")"); + } else { + plusDistribueValue.setText("Personne"); + } + } else { + // Pas de stats disponibles + gorgeesTotalValue.setText("0"); + plusBuValue.setText("Personne"); + plusDistribueValue.setText("Personne"); } // Afficher un message de félicitations diff --git a/app/src/main/java/com/example/boidelov3/Jeux.java b/app/src/main/java/com/example/boidelov3/Jeux.java index 4c43e3f..66af488 100644 --- a/app/src/main/java/com/example/boidelov3/Jeux.java +++ b/app/src/main/java/com/example/boidelov3/Jeux.java @@ -6,6 +6,7 @@ import android.content.SharedPreferences; import android.graphics.Color; import android.os.Bundle; import android.text.Html; +import android.util.Log; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.Button; @@ -19,6 +20,10 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import com.example.boidelov3.data.PlayerStats; +import com.example.boidelov3.data.QuestionCategory; +import com.example.boidelov3.utils.SoundManager; +import com.example.boidelov3.utils.ErrorHandler; import com.google.android.material.button.MaterialButton; import com.google.gson.Gson; @@ -26,6 +31,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -48,11 +55,17 @@ public class Jeux extends AppCompatActivity { private LinearLayout questionIndicator; private ImageView indicatorIcon; private TextView indicatorText; + private View rootLayout; // Layout racine pour la couleur de fond // Données private Questions questions; private List toutlesjoueurs; private List questionsAvecManches = new ArrayList<>(); + private Map playerStatsMap; // Statistiques des joueurs + private SoundManager soundManager; // Gestionnaire de sons + private OpenAIService openAIService; // Service OpenAI + private OpenAIService.AIProvider aiProvider = OpenAIService.AIProvider.OPENAI; // Provider IA + private List generatedAIQuestions; // Questions générées par l'IA // Générateur aléatoire unique pour toute l'activité private final Random random = new Random(); @@ -71,6 +84,22 @@ public class Jeux extends AppCompatActivity { private String currentQuestionText = ""; private boolean isMancheActive = false; private boolean isFinishingGame = false; // Flag pour empêcher les appels multiples pendant la fin du jeu + private int questionsSinceLastAI = 0; // Compteur pour le ratio IA + + // Constantes pour les nombres magiques + private static final int MIN_DEFI_ROUNDS = 3; // Minimum 3 manches pour les défis + private static final int MAX_DEFI_ROUNDS_RANDOM = 5; // Max 5 tours aléatoires en plus (3-8 tours au total) + private static final int MIN_MANCHES_COUNT = 1; + private static final int PREGENERATED_AI_QUESTIONS = 2; + private static final int MIN_AI_QUESTION_STOCK = 2; + private static final int MIN_AI_GORGEE = 1; // Minimum 1 gorgée + private static final int MAX_AI_GORGEE_ADDITIONAL = 3; + private static final int ANIMATION_DURATION_SHORT_MS = 300; + private static final int BACKGROUND_COLOR_DELAY_MS = 1000; + private static final int END_GAME_DELAY_MS = 3000; + private static final int RANDOM_PLAYER_SELECTION_COUNT = 3; + private static final int TWO_PLAYERS = 2; + private static final int ONE_PLAYER = 1; // Clés pour sauvegarde d'état private static final String KEY_TOTAL_QUESTIONS = "total_questions_asked"; @@ -88,6 +117,16 @@ public class Jeux extends AppCompatActivity { // Récupération des données passées par l'activité précédente toutlesjoueurs = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR"); + + // Initialiser les statistiques des joueurs + initializePlayerStats(); + + // Initialiser le gestionnaire de sons + soundManager = SoundManager.getInstance(this); + + // Initialiser les services IA si activés + generatedAIQuestions = new ArrayList<>(); + nombreQuestions = getIntent().getIntExtra("EXTRA_NOMBRE_QUESTIONS", 50); ajoutGorgees = getIntent().getIntExtra("EXTRA_AJOUT_GORGEE", 0); ratiOpenai = getIntent().getIntExtra("EXTRA_RATIO_OPENAI", 0); @@ -95,6 +134,32 @@ public class Jeux extends AppCompatActivity { openAI = getIntent().getBooleanExtra("EXTRA_OPENAI", false); keyOpenai = getIntent().getStringExtra("EXTRA_KEY_OPENAI"); + // Récupérer le provider IA + String providerName = getIntent().getStringExtra("EXTRA_AI_PROVIDER"); + if (providerName != null) { + try { + aiProvider = OpenAIService.AIProvider.valueOf(providerName); + } catch (IllegalArgumentException e) { + aiProvider = OpenAIService.AIProvider.OPENAI; + } + } + + if (openAI && keyOpenai != null && !keyOpenai.isEmpty()) { + // Valider la clé API avant de créer le service + if (OpenAIService.validateApiKey(keyOpenai, aiProvider)) { + openAIService = new OpenAIService(keyOpenai, aiProvider); + // Prégénérer quelques questions IA pour avoir un stock au démarrage + for (int i = 0; i < PREGENERATED_AI_QUESTIONS; i++) { + generateAIQuestion(); + } + } else { + // Clé API invalide : désactiver l'IA et afficher un message + Log.w("Jeux", "Clé API invalide, désactivation de la génération IA"); + openAI = false; + Toast.makeText(this, "Clé API invalide. Vérifiez votre configuration.", Toast.LENGTH_LONG).show(); + } + } + // Charger les questions depuis le JSON loadQuestions(); @@ -129,6 +194,7 @@ public class Jeux extends AppCompatActivity { questionIndicator = findViewById(R.id.questionIndicator); indicatorIcon = findViewById(R.id.indicatorIcon); indicatorText = findViewById(R.id.indicatorText); + rootLayout = findViewById(R.id.rootLayout); // Appliquer les animations aux boutons BoideloAnimationUtils.applyButtonPressAnimation(suivantButton); @@ -136,10 +202,6 @@ public class Jeux extends AppCompatActivity { // Cacher le bouton Passé au début (uniquement visible pendant les défis) skipButton.setVisibility(View.GONE); - - // Initialiser la couleur de fond (respecte le mode jour/nuit) - int backgroundColor = ContextCompat.getColor(this, R.color.game_normal); - getWindow().getDecorView().setBackgroundColor(backgroundColor); } /** @@ -157,8 +219,21 @@ public class Jeux extends AppCompatActivity { Gson gson = new Gson(); questions = gson.fromJson(json, Questions.class); } catch (IOException ex) { - ex.printStackTrace(); - Toast.makeText(this, "Erreur de chargement des questions", Toast.LENGTH_SHORT).show(); + String operation = "Chargement du fichier question.json depuis les assets"; + String details = "Impossible de lire ou de parser le fichier de questions"; + ErrorHandler.showError(this, "Jeux", operation, details + ": " + ex.getMessage(), ex); + } + } + + /** + * Initialise les statistiques des joueurs + */ + private void initializePlayerStats() { + playerStatsMap = new HashMap<>(); + if (toutlesjoueurs != null) { + for (String joueur : toutlesjoueurs) { + playerStatsMap.put(joueur, new PlayerStats(joueur)); + } } } @@ -177,6 +252,7 @@ public class Jeux extends AppCompatActivity { private void setupButtonListeners() { suivantButton.setOnClickListener(v -> { BoideloAnimationUtils.triggerHapticFeedback(this); + // Plus de son de clic - juste la vibration haptique updateQuestion(); }); @@ -211,20 +287,45 @@ public class Jeux extends AppCompatActivity { } // Vérifier si toutes les questions ont été posées - if (totalQuestionsAsked >= nombreQuestions) { - // Vérifier s'il y a encore des manches actives - if (!questionsAvecManches.isEmpty()) { - // Afficher le message de fin de manche et terminer - showFinalMancheEndMessage(); - return; - } - endGame(); + if (shouldEndGame()) { + handleGameEnd(); return; } // Gérer les questions avec manches actives + boolean hasMancheActive = processActiveManches(); + + // Afficher une nouvelle question (que ce soit pendant ou hors manche) + displayNewQuestion(hasMancheActive); + } + + /** + * Vérifie si le jeu doit se terminer + */ + private boolean shouldEndGame() { + return totalQuestionsAsked >= nombreQuestions; + } + + /** + * Gère la fin du jeu + */ + private void handleGameEnd() { + // Vérifier s'il y a encore des manches actives + if (!questionsAvecManches.isEmpty()) { + // Afficher le message de fin de manche et terminer + showFinalMancheEndMessage(); + } else { + endGame(); + } + } + + /** + * Traite les manches actives et retourne true si une manche est active + */ + private boolean processActiveManches() { Iterator iterator = questionsAvecManches.iterator(); boolean hasMancheActive = false; + while (iterator.hasNext()) { Question mancheQuestion = iterator.next(); mancheQuestion.setManchesRestantes(mancheQuestion.getManchesRestantes() - 1); @@ -243,7 +344,13 @@ public class Jeux extends AppCompatActivity { } } - // Afficher une nouvelle question (que ce soit pendant ou hors manche) + return hasMancheActive; + } + + /** + * Affiche une nouvelle question + */ + private void displayNewQuestion(boolean hasMancheActive) { Question question = getRandomQuestion(); if (question != null) { displayQuestion(question, hasMancheActive); @@ -278,7 +385,7 @@ public class Jeux extends AppCompatActivity { mancheCounterTextView.postDelayed(() -> { int defaultColor = ContextCompat.getColor(this, R.color.game_normal); BoideloAnimationUtils.animateBackgroundColor(getWindow().getDecorView(), defaultColor); - }, 1000); + }, BACKGROUND_COLOR_DELAY_MS); } /** @@ -312,7 +419,7 @@ public class Jeux extends AppCompatActivity { mancheCounterTextView.postDelayed(() -> { questionsAvecManches.clear(); endGame(); - }, 3000); + }, END_GAME_DELAY_MS); } /** @@ -356,67 +463,130 @@ public class Jeux extends AppCompatActivity { showQuestionIndicator(R.drawable.ic_manche, "Manche en cours"); // Animation du compteur - BoideloAnimationUtils.pulse(mancheCounterTextView, 300); + BoideloAnimationUtils.pulse(mancheCounterTextView, ANIMATION_DURATION_SHORT_MS); } /** * Affiche une nouvelle question */ private void displayQuestion(Question question, boolean hasMancheActive) { - questionTextView.setText(Html.fromHtml(question.getQuestion(), Html.FROM_HTML_MODE_LEGACY)); + // Afficher le texte de la question + displayQuestionText(question); // Masquer ou afficher la question de manche selon l'état + handleMancheQuestionVisibility(hasMancheActive); + + // Détecter et afficher la catégorie de la question + QuestionCategory.Category category = QuestionCategory.detectCategory(question); + applyCategoryStyle(category); + + // Déterminer et afficher l'indicateur de joueurs + String indicatorText = determinePlayerCountIndicator(question, category); + showIndicatorText(indicatorText); + } + + /** + * Affiche le texte de la question + */ + private void displayQuestionText(Question question) { + questionTextView.setText(Html.fromHtml(question.getQuestion(), Html.FROM_HTML_MODE_LEGACY)); + } + + /** + * Gère la visibilité de la question de manche + */ + private void handleMancheQuestionVisibility(boolean hasMancheActive) { if (!hasMancheActive) { mancheQuestionText.setVisibility(View.GONE); } + } - // Déterminer le type de question et animer le fond en conséquence - String questionText = question.getQuestion(); - + /** + * Applique le style de catégorie (couleur de fond, etc.) + */ + private void applyCategoryStyle(QuestionCategory.Category category) { // Réinitialiser les indicateurs questionIndicator.setVisibility(View.GONE); mancheCounterTextView.setVisibility(View.GONE); - // Fond par défaut (respecte le mode jour/nuit) - int defaultColor = ContextCompat.getColor(this, R.color.game_normal); - BoideloAnimationUtils.animateBackgroundColor( - getWindow().getDecorView(), - defaultColor - ); + // Appliquer la couleur de fond + int categoryColor = QuestionCategory.getColorForCategory(category); + rootLayout.setBackgroundColor(categoryColor); + } - // Vérifier le type de question + /** + * Détermine le texte de l'indicateur en fonction du nombre de joueurs et de la catégorie + */ + private String determinePlayerCountIndicator(Question question, QuestionCategory.Category category) { + String questionText = question.getQuestion(); boolean isJoueurs1 = questionText.contains(""); boolean isJoueurs2 = questionText.contains(""); boolean isJoueurs3 = questionText.contains(""); - boolean hasManches = questionText.contains(""); - if (hasManches) { - int blueColor = ContextCompat.getColor(this, R.color.game_question_manche); - BoideloAnimationUtils.animateBackgroundColor(getWindow().getDecorView(), blueColor); - showQuestionIndicator(R.drawable.ic_manche, "Défi à manches"); - } else if (isJoueurs1 && isJoueurs2 && isJoueurs3) { - int greenDarkColor = ContextCompat.getColor(this, R.color.game_question_3players); - BoideloAnimationUtils.animateBackgroundColor(getWindow().getDecorView(), greenDarkColor); - showQuestionIndicator(R.drawable.ic_player_three, "3 joueurs"); + String categoryEmoji = getCategoryEmoji(category); + String categoryName = QuestionCategory.getNameForCategory(category); + + // Priorité aux indications de nombre de joueurs si applicable + if (isJoueurs1 && isJoueurs2 && isJoueurs3) { + return "3 joueurs"; } else if (isJoueurs1 && isJoueurs2) { - int greenColor = ContextCompat.getColor(this, R.color.game_question_2players); - BoideloAnimationUtils.animateBackgroundColor(getWindow().getDecorView(), greenColor); - showQuestionIndicator(R.drawable.ic_player_two, "2 joueurs"); + return "2 joueurs"; } else if (isJoueurs1) { - int greenLightColor = ContextCompat.getColor(this, R.color.game_question_1player); - BoideloAnimationUtils.animateBackgroundColor(getWindow().getDecorView(), greenLightColor); - showQuestionIndicator(R.drawable.ic_player_one, "1 joueur"); + return "1 joueur"; + } else { + // Juste l'emoji et le nom de la catégorie + return categoryEmoji + " " + categoryName; } } + /** + * Affiche l'indicateur avec le texte spécifié + */ + private void showIndicatorText(String textToShow) { + questionIndicator.setVisibility(View.VISIBLE); + indicatorIcon.setVisibility(View.GONE); + indicatorText.setText(textToShow); + } + + /** + * Retourne l'emoji associé à une catégorie + */ + private String getCategoryEmoji(QuestionCategory.Category category) { + switch (category) { + case CIBLAGE: + return "🎯"; + case CLASSEMENT: + return "👑"; + case JUGEMENT: + return "⚖️"; + case DUEL: + return "🤝"; + case INTERACTIF: + return "🎮"; + case DEFI_MANCHES: + return "🔥"; + case VARIANTE: + return "❓"; + case CALIENTE: + return "😈"; + case VOTE: + return "🗳️"; + case CLASSIQUE: + default: + return "⚪"; + } + } + + /** * Affiche l'indicateur de type de question */ private void showQuestionIndicator(int iconRes, String text) { indicatorIcon.setImageResource(iconRes); + indicatorIcon.setVisibility(View.VISIBLE); indicatorText.setText(text); questionIndicator.setVisibility(View.VISIBLE); - BoideloAnimationUtils.fadeIn(questionIndicator, 300); + BoideloAnimationUtils.fadeIn(questionIndicator, ANIMATION_DURATION_SHORT_MS); } /** @@ -456,6 +626,9 @@ public class Jeux extends AppCompatActivity { * Termine la partie et affiche l'écran de fin */ private void endGame() { + // Jouer le son de fin + if (soundManager != null) soundManager.playFin(); + // Sauvegarder les statistiques de la partie saveGameStats(); @@ -464,6 +637,11 @@ public class Jeux extends AppCompatActivity { intent.putExtra("EXTRA_QUESTIONS_PLAYED", totalQuestionsAsked); intent.putExtra("EXTRA_PLAYERS_COUNT", toutlesjoueurs != null ? toutlesjoueurs.size() : 0); intent.putStringArrayListExtra("EXTRA_PLAYERS", (ArrayList) toutlesjoueurs); + + // Passer les statistiques des joueurs + ArrayList playerStatsList = new ArrayList<>(playerStatsMap.values()); + intent.putParcelableArrayListExtra("EXTRA_PLAYER_STATS", playerStatsList); + startActivity(intent); // Animation de transition @@ -506,7 +684,7 @@ public class Jeux extends AppCompatActivity { public List TroisJoueurAleatoire(List toutlesjoueurs) { Set setJoueur = new HashSet<>(); - while (setJoueur.size() < 3 && toutlesjoueurs.size() >= 3) { + while (setJoueur.size() < RANDOM_PLAYER_SELECTION_COUNT && toutlesjoueurs.size() >= RANDOM_PLAYER_SELECTION_COUNT) { setJoueur.add(toutlesjoueurs.get(random.nextInt(toutlesjoueurs.size()))); } @@ -528,10 +706,129 @@ public class Jeux extends AppCompatActivity { return null; } + /** + * Génère une nouvelle question via l'IA OpenAI + */ + private void generateAIQuestion() { + if (openAIService == null) { + return; + } + + openAIService.generateQuestion(toutlesjoueurs, ajoutGorgees, new OpenAIService.OpenAICallback() { + @Override + public void onSuccess(String questionText) { + if (questionText != null && !questionText.isEmpty()) { + // Créer une nouvelle question à partir du texte généré + Question aiQuestion = createAIQuestion(questionText); + if (aiQuestion != null) { + generatedAIQuestions.add(aiQuestion); + // Afficher un petit indicateur que c'est une question IA + Toast.makeText(Jeux.this, "✨ Question IA générée !", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void onError(String errorMessage) { + // En cas d'erreur, on continue avec les questions JSON + // Pas de toast pour ne pas interrompre le jeu + android.util.Log.e("OpenAI", "Erreur génération question: " + errorMessage); + } + }); + } + + /** + * Crée un objet Question à partir du texte généré par l'IA + */ + private Question createAIQuestion(String questionText) { + try { + Question question = new Question(); + question.setId(-1); // ID négatif pour les questions IA + question.setQuestion(questionText); + question.setGorger(MIN_AI_GORGEE + random.nextInt(MAX_AI_GORGEE_ADDITIONAL)); // 2-4 gorgées par défaut + question.setDistribution(random.nextBoolean()); + question.setRecois(!question.isDistribution() || random.nextBoolean()); + + // Détecter si c'est une question de vote + if (questionText.toLowerCase().contains("votez") || + questionText.toLowerCase().contains("minorité") || + questionText.toLowerCase().contains("majorité")) { + question.setDistribution(false); + question.setRecois(true); + } + + return question; + } catch (Exception e) { + android.util.Log.e("OpenAI", "Erreur création question IA: " + e.getMessage()); + return null; + } + } + /** * Obtient une question aléatoire qui n'a pas encore été posée + * Intègre les questions générées par l'IA selon le ratio configuré */ private Question getRandomQuestion() { + // Incrémenter le compteur de questions depuis la dernière IA + questionsSinceLastAI++; + + // Vérifier si on doit utiliser une question IA + Question aiQuestion = tryGetAIQuestion(); + if (aiQuestion != null) { + return aiQuestion; + } + + // Générer une question IA en arrière-plan si le stock est bas + if (shouldGenerateAIInBackground()) { + generateAIQuestion(); + } + + // Sinon, utiliser une question du JSON + return getJSONQuestion(); + } + + /** + * Tente de récupérer une question IA si les conditions sont remplies + */ + private Question tryGetAIQuestion() { + if (!shouldUseAIQuestion()) { + return null; + } + + if (generatedAIQuestions.isEmpty()) { + return null; + } + + Question aiQuestion = generatedAIQuestions.remove(0); + questionsSinceLastAI = 0; // Réinitialiser le compteur + + // Traiter la question IA + processQuestion(aiQuestion); + + // Générer en arrière-plan la prochaine question IA pour avoir un stock + generateAIQuestion(); + + return aiQuestion; + } + + /** + * Vérifie si on doit utiliser une question IA selon le ratio + */ + private boolean shouldUseAIQuestion() { + return ratiOpenai > 0 && openAIService != null && questionsSinceLastAI >= ratiOpenai; + } + + /** + * Vérifie si on doit générer une question IA en arrière-plan + */ + private boolean shouldGenerateAIInBackground() { + return openAIService != null && generatedAIQuestions.size() < MIN_AI_QUESTION_STOCK; + } + + /** + * Récupère une question depuis le fichier JSON + */ + private Question getJSONQuestion() { if (questions == null) { return null; } @@ -539,18 +836,44 @@ public class Jeux extends AppCompatActivity { SharedPreferences prefs = getSharedPreferences("app", Context.MODE_PRIVATE); Set askedQuestions = prefs.getStringSet("askedQuestions", new HashSet<>()); + List unaskedQuestions = getUnaskedQuestions(askedQuestions); + + if (unaskedQuestions.isEmpty()) { + return null; + } + + List availableQuestions = filterAvailableQuestions(unaskedQuestions); + + if (availableQuestions.isEmpty()) { + return null; + } + + Question question = selectRandomQuestion(availableQuestions); + markQuestionAsAsked(question, askedQuestions, prefs); + + // Traiter la question + processQuestion(question); + + return question; + } + + /** + * Récupère la liste des questions non posées + */ + private List getUnaskedQuestions(Set askedQuestions) { List unaskedQuestions = new ArrayList<>(); for (Question question : questions.getQuestions()) { if (!askedQuestions.contains(String.valueOf(question.getId()))) { unaskedQuestions.add(question); } } + return unaskedQuestions; + } - if (unaskedQuestions.isEmpty()) { - return null; - } - - // Filtrer les questions : si un défi est en cours, exclure celles avec + /** + * Filtre les questions disponibles selon qu'un défi est en cours ou non + */ + private List filterAvailableQuestions(List unaskedQuestions) { List availableQuestions = new ArrayList<>(); boolean mancheActive = !questionsAvecManches.isEmpty(); @@ -566,23 +889,26 @@ public class Jeux extends AppCompatActivity { } } - // Si aucune question disponible (que des questions avec alors qu'un défi est en cours) - if (availableQuestions.isEmpty()) { - return null; - } + return availableQuestions; + } - Question question = availableQuestions.get(random.nextInt(availableQuestions.size())); + /** + * Sélectionne une question aléatoire parmi celles disponibles + */ + private Question selectRandomQuestion(List availableQuestions) { + return availableQuestions.get(random.nextInt(availableQuestions.size())); + } + + /** + * Marque une question comme posée et sauvegarde l'état + */ + private void markQuestionAsAsked(Question question, Set askedQuestions, SharedPreferences prefs) { askedQuestions.add(String.valueOf(question.getId())); // Sauvegarder les questions posées SharedPreferences.Editor editor = prefs.edit(); editor.putStringSet("askedQuestions", askedQuestions); editor.apply(); - - // Traiter la question - processQuestion(question); - - return question; } /** @@ -592,18 +918,42 @@ public class Jeux extends AppCompatActivity { String questionText = question.getQuestion(); // Remplacer les variantes + questionText = processVariantes(question, questionText); + + // Gérer les manches + questionText = processManches(question, questionText); + + // Remplacer les joueurs et récupérer le nom du joueur J1 pour les stats + PlayerSelectionResult playerResult = replacePlayers(questionText); + questionText = playerResult.questionText; + + // Ajouter les gorgées et tracker les stats (en passant le nombre de joueurs pour l'accord) + GorgeeResult gorgeeResult = processGorgees(question, questionText, playerResult.playerCount); + questionText = gorgeeResult.questionText; + + // Mettre à jour les statistiques du joueur J1 + updatePlayerStats(playerResult.j1Name, gorgeeResult); + + question.setQuestion(questionText); + } + + /** + * Remplace les variantes dans la question + */ + private String processVariantes(Question question, String questionText) { if (question.getVariante() != null && !question.getVariante().isEmpty()) { String chosenVariante = question.getVariante().get(random.nextInt(question.getVariante().size())); questionText = questionText.replace("", chosenVariante); } + return questionText; + } - // Gérer les manches + /** + * Gère les manches pour les défis + */ + private String processManches(Question question, String questionText) { if (questionText.contains("")) { - int nbaleatoiremanches = random.nextInt(10) + 5 + durationDefis; - // S'assurer qu'il y a au moins 1 manche - if (nbaleatoiremanches < 1) { - nbaleatoiremanches = 1; - } + int nbaleatoiremanches = calculateManchesCount(); questionText = questionText.replace("", String.valueOf(nbaleatoiremanches)); question.setManchesRestantes(nbaleatoiremanches); @@ -611,51 +961,181 @@ public class Jeux extends AppCompatActivity { question.setArretMessageManche("Fin de défi!\n" + stopid); questionsAvecManches.add(question); + // Jouer le son de manche + if (soundManager != null) soundManager.playManche(); + // Afficher le bouton Passé pendant les défis skipButton.setVisibility(View.VISIBLE); - BoideloAnimationUtils.fadeIn(skipButton, 300); + BoideloAnimationUtils.fadeIn(skipButton, ANIMATION_DURATION_SHORT_MS); } + return questionText; + } - // Remplacer les joueurs + /** + * Calcule le nombre de manches pour un défi + */ + private int calculateManchesCount() { + int nbaleatoiremanches = random.nextInt(MAX_DEFI_ROUNDS_RANDOM) + MIN_DEFI_ROUNDS + durationDefis; + // S'assurer qu'il y a au moins 1 manche + return Math.max(nbaleatoiremanches, MIN_MANCHES_COUNT); + } + + /** + * Résultat du remplacement des joueurs + */ + private static class PlayerSelectionResult { + String questionText; + String j1Name; + int playerCount; // Nombre de joueurs sélectionnés pour l'accord du verbe + + PlayerSelectionResult(String questionText, String j1Name, int playerCount) { + this.questionText = questionText; + this.j1Name = j1Name; + this.playerCount = playerCount; + } + } + + /** + * Remplace les placeholders de joueurs dans la question + */ + private PlayerSelectionResult replacePlayers(String questionText) { boolean isJoueurs1 = questionText.contains(""); boolean isJoueurs2 = questionText.contains(""); boolean isJoueurs3 = questionText.contains(""); + String j1Name = null; + int playerCount = 0; + if (isJoueurs1 || isJoueurs2 || isJoueurs3) { List aleatoirejoueurs = TroisJoueurAleatoire(toutlesjoueurs); - if (isJoueurs1 && isJoueurs2 && isJoueurs3 && aleatoirejoueurs.size() >= 3) { - questionText = questionText.replace("", aleatoirejoueurs.get(0)); - questionText = questionText.replace("", aleatoirejoueurs.get(1)); - questionText = questionText.replace("", aleatoirejoueurs.get(2)); - } else if (isJoueurs1 && isJoueurs2 && aleatoirejoueurs.size() >= 2) { - questionText = questionText.replace("", aleatoirejoueurs.get(0)); - questionText = questionText.replace("", aleatoirejoueurs.get(1)); - } else if (isJoueurs1 && aleatoirejoueurs.size() >= 1) { - questionText = questionText.replace("", aleatoirejoueurs.get(0)); + if (isJoueurs1 && isJoueurs2 && isJoueurs3 && aleatoirejoueurs.size() >= RANDOM_PLAYER_SELECTION_COUNT) { + playerCount = 3; + j1Name = aleatoirejoueurs.get(0); + questionText = questionText.replace("", ErrorHandler.escapeHtml(j1Name)); + questionText = questionText.replace("", ErrorHandler.escapeHtml(aleatoirejoueurs.get(1))); + questionText = questionText.replace("", ErrorHandler.escapeHtml(aleatoirejoueurs.get(2))); + } else if (isJoueurs1 && isJoueurs2 && aleatoirejoueurs.size() >= TWO_PLAYERS) { + playerCount = 2; + j1Name = aleatoirejoueurs.get(0); + questionText = questionText.replace("", ErrorHandler.escapeHtml(j1Name)); + questionText = questionText.replace("", ErrorHandler.escapeHtml(aleatoirejoueurs.get(1))); + } else if (isJoueurs1 && aleatoirejoueurs.size() >= ONE_PLAYER) { + playerCount = 1; + j1Name = aleatoirejoueurs.get(0); + questionText = questionText.replace("", ErrorHandler.escapeHtml(j1Name)); } } - // Ajouter les gorgées + return new PlayerSelectionResult(questionText, j1Name, playerCount); + } + + /** + * Résultat du traitement des gorgées + */ + private static class GorgeeResult { + String questionText; + int totalGorgees; + boolean isBois; + boolean isDistribue; + + GorgeeResult(String questionText, int totalGorgees, boolean isBois, boolean isDistribue) { + this.questionText = questionText; + this.totalGorgees = totalGorgees; + this.isBois = isBois; + this.isDistribue = isDistribue; + } + } + + /** + * Traite les gorgées pour une question + */ + private GorgeeResult processGorgees(Question question, String questionText, int playerCount) { + int totalGorgees = 0; + boolean isBois = false; + boolean isDistribue = false; + if (question.isDistribution() || question.isRecois()) { - if (question.isRecois() && question.isDistribution()) { - boolean rand = random.nextBoolean(); - if (rand) { - questionText = questionText.concat(" bois"); - } else { - questionText = questionText.concat(" distribue"); - } - } else if (question.isRecois()) { - questionText = questionText.concat(" bois"); - } else if (question.isDistribution()) { - questionText = questionText.concat(" distribue"); - } + totalGorgees = question.getGorger() + ajoutGorgees; - questionText = questionText.concat(" " + (question.getGorger() + ajoutGorgees) + " gorgée" + - ((question.getGorger() + ajoutGorgees) > 1 ? "s" : "") + "."); + ActionChoiceResult actionResult = determineActionChoice(question, questionText, playerCount); + questionText = actionResult.questionText; + isBois = actionResult.isBois; + isDistribue = actionResult.isDistribue; + + questionText = appendGorgeeCount(questionText, totalGorgees); } - question.setQuestion(questionText); + return new GorgeeResult(questionText, totalGorgees, isBois, isDistribue); + } + + /** + * Résultat du choix d'action (bois/distribue) + */ + private static class ActionChoiceResult { + String questionText; + boolean isBois; + boolean isDistribue; + + ActionChoiceResult(String questionText, boolean isBois, boolean isDistribue) { + this.questionText = questionText; + this.isBois = isBois; + this.isDistribue = isDistribue; + } + } + + /** + * Détermine l'action (boit/boivent ou distribue/distribuent) selon le nombre de joueurs + */ + 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"; + + if (question.isRecois() && question.isDistribution()) { + boolean rand = random.nextBoolean(); + if (rand) { + questionText = questionText.concat(" " + boisVerb + ""); + isBois = true; + } else { + questionText = questionText.concat(" " + distribueVerb + ""); + isDistribue = true; + } + } else if (question.isRecois()) { + questionText = questionText.concat(" " + boisVerb + ""); + isBois = true; + } else if (question.isDistribution()) { + questionText = questionText.concat(" " + distribueVerb + ""); + isDistribue = true; + } + + return new ActionChoiceResult(questionText, isBois, isDistribue); + } + + /** + * Ajoute le nombre de gorgées au texte de la question + */ + private String appendGorgeeCount(String questionText, int totalGorgees) { + String plural = totalGorgees > 1 ? "s" : ""; + return questionText.concat(" " + totalGorgees + " gorgée" + plural + "."); + } + + /** + * Met à jour les statistiques du joueur J1 + */ + private void updatePlayerStats(String j1Name, GorgeeResult gorgeeResult) { + if (j1Name != null && playerStatsMap != null && playerStatsMap.containsKey(j1Name)) { + PlayerStats stats = playerStatsMap.get(j1Name); + if (gorgeeResult.isBois) { + stats.addGorgeesBuves(gorgeeResult.totalGorgees); + } + if (gorgeeResult.isDistribue) { + stats.addGorgeesDistribuees(gorgeeResult.totalGorgees); + } + } } /** @@ -760,4 +1240,17 @@ public class Jeux extends AppCompatActivity { } return null; } + + @Override + protected void onDestroy() { + super.onDestroy(); + // Libérer les ressources du SoundManager + if (soundManager != null) { + soundManager.release(); + } + // Libérer les ressources du service OpenAI + if (openAIService != null) { + openAIService.shutdown(); + } + } } diff --git a/app/src/main/java/com/example/boidelov3/JeuxParametres.java b/app/src/main/java/com/example/boidelov3/JeuxParametres.java index b495fbb..9880dcb 100644 --- a/app/src/main/java/com/example/boidelov3/JeuxParametres.java +++ b/app/src/main/java/com/example/boidelov3/JeuxParametres.java @@ -8,6 +8,9 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CompoundButton; @@ -20,6 +23,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import com.example.boidelov3.utils.ErrorHandler; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -35,8 +40,11 @@ public class JeuxParametres extends AppCompatActivity { private SeekBar seekBar1, seekBar2, seekBar3, seekBarDuration; private TextView textView1, textView2, textView5, textViewRatioGen, questionCountValue, gorgeesValue, durationValue; private SwitchMaterial checkBoxGPT; + private Button buttonTestApi; private EditText editText, editTextKeyGPT; + private AutoCompleteTextView autoCompleteProvider; private String keyGPT; + private OpenAIService.AIProvider selectedProvider = OpenAIService.AIProvider.OPENAI; private int nbQuestions; private List toutlesjoueurs; @@ -57,25 +65,67 @@ public class JeuxParametres extends AppCompatActivity { textView2 = findViewById(R.id.textView2); textView5 = findViewById(R.id.textView5); editTextKeyGPT = findViewById(R.id.editTextGPT); + autoCompleteProvider = findViewById(R.id.autoCompleteProvider); + buttonTestApi = findViewById(R.id.ButtonTestApi); textViewRatioGen = findViewById(R.id.textViewRatioGen); questionCountValue = findViewById(R.id.questionCountValue); gorgeesValue = findViewById(R.id.gorgeesValue); durationValue = findViewById(R.id.durationValue); + // Configuration du dropdown pour le provider IA + String[] providers = new String[]{ + OpenAIService.AIProvider.OPENAI.getDisplayName(), + OpenAIService.AIProvider.OPENROUTER.getDisplayName(), + OpenAIService.AIProvider.ZAI.getDisplayName() + }; + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, providers); + autoCompleteProvider.setAdapter(adapter); + + // Charger le provider sauvegardé + SharedPreferences providerPrefs = getSharedPreferences("MyPrefs", MODE_PRIVATE); + String savedProvider = providerPrefs.getString("aiProvider", OpenAIService.AIProvider.OPENAI.getDisplayName()); + autoCompleteProvider.setText(savedProvider, false); + + // Définir le provider sélectionné + if (savedProvider.equals(OpenAIService.AIProvider.OPENROUTER.getDisplayName())) { + selectedProvider = OpenAIService.AIProvider.OPENROUTER; + } else if (savedProvider.equals(OpenAIService.AIProvider.ZAI.getDisplayName())) { + selectedProvider = OpenAIService.AIProvider.ZAI; + } else { + selectedProvider = OpenAIService.AIProvider.OPENAI; + } + + // Listener pour le changement de provider + autoCompleteProvider.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + String selected = (String) parent.getItemAtPosition(position); + SharedPreferences.Editor editor = providerPrefs.edit(); + editor.putString("aiProvider", selected); + editor.apply(); + + if (selected.equals(OpenAIService.AIProvider.OPENROUTER.getDisplayName())) { + selectedProvider = OpenAIService.AIProvider.OPENROUTER; + } else if (selected.equals(OpenAIService.AIProvider.ZAI.getDisplayName())) { + selectedProvider = OpenAIService.AIProvider.ZAI; + } else { + selectedProvider = OpenAIService.AIProvider.OPENAI; + } + } + }); + // Initialiser les TextView avec les valeurs par défaut int initialQuestions = 50; int initialGorgees = 0; int initialRatio = 8; - int initialDuration = 0; + int initialDuration = 0; // 0 pour avoir 3-8 tours par défaut (MIN_DEFI_ROUNDS=3) questionCountValue.setText(String.valueOf(initialQuestions)); gorgeesValue.setText(String.valueOf(initialGorgees)); - durationValue.setText("0"); // Afficher 0 par défaut (pas de signe pour 0) + durationValue.setText("0"); // Afficher 0 par défaut pour avoir 3-8 tours textView5.setText("Palier : Grosse merde"); textViewRatioGen.setText("Ratio BDD/OPENAI : 1/" + initialRatio); - Button buttonTestApi = findViewById(R.id.ButtonTestApi); - // Configuration de la seekBar1 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { seekBar1.setMin(20); @@ -100,7 +150,7 @@ public class JeuxParametres extends AppCompatActivity { seekBarDuration.setMin(-5); // Permet un offset négatif jusqu'à -5 } seekBarDuration.setMax(15); - seekBarDuration.setProgress(0); // Valeur par défaut à 0 + seekBarDuration.setProgress(0); // Valeur par défaut à 0 pour avoir 3-8 tours (MIN_DEFI_ROUNDS=3) // Configuration des listeners pour les seekBars seekBar1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @@ -185,13 +235,23 @@ public class JeuxParametres extends AppCompatActivity { // Configuration du checkBox // Q : IL sert à quoi ? // R : Il sert à activer/désactiver les vues en dessous + Button buttonTestApi = findViewById(R.id.ButtonTestApi); + checkBoxGPT = findViewById(R.id.checkBoxGPT); checkBoxGPT.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Activation/désactivation des vues en fonction de l'état du checkBox - editTextKeyGPT.setEnabled(isChecked); - //editText.setEnabled(isChecked); + autoCompleteProvider.setEnabled(isChecked); + // Pour le champ API key : on garde le layout activé pour le toggle password, + // mais on désactive l'édition du texte + editTextKeyGPT.setFocusable(isChecked); + editTextKeyGPT.setFocusableInTouchMode(isChecked); + editTextKeyGPT.setClickable(isChecked); + editTextKeyGPT.setCursorVisible(isChecked); + if (!isChecked) { + editTextKeyGPT.clearFocus(); + } textViewRatioGen.setEnabled(isChecked); seekBar3.setEnabled(isChecked); buttonTestApi.setEnabled(isChecked); @@ -270,51 +330,99 @@ public class JeuxParametres extends AppCompatActivity { } public void onClickButtonTestAPI(View view) { - String apiKey = editTextKeyGPT.getText().toString(); - // Créer un client OkHttpClient pour effectuer la requête - OkHttpClient client = new OkHttpClient(); - // Construire la requête d'essai vers l'API - Request request = new Request.Builder() - .url("https://api.openai.com/v1/engines/davinci") // Endpoint d'essai, vous pouvez le modifier selon vos besoins - .header("Authorization", "Bearer " + apiKey) // Ajouter la clé API dans l'en-tête de la requête + if (apiKey == null || apiKey.isEmpty()) { + Toast.makeText(this, "Veuillez entrer une clé API", Toast.LENGTH_SHORT).show(); + return; + } + + // Créer un client OkHttpClient pour effectuer la requête + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(10, java.util.concurrent.TimeUnit.SECONDS) + .build(); + + // Déterminer l'URL, le modèle et le format selon le provider sélectionné + String testUrl; + String testModel; + String jsonBody; + boolean isAnthropicFormat = (selectedProvider == OpenAIService.AIProvider.ZAI); + + switch (selectedProvider) { + case OPENROUTER: + testUrl = "https://openrouter.ai/api/v1/chat/completions"; + testModel = "openai/gpt-3.5-turbo"; + // Format OpenAI + jsonBody = "{\"model\":\"" + testModel + "\",\"messages\":[{\"role\":\"user\",\"content\":\"Test\"}],\"max_tokens\":5}"; + break; + case ZAI: + testUrl = "https://api.z.ai/v1/messages"; + testModel = "claude-3-5-sonnet"; + // Format Anthropic + jsonBody = "{\"model\":\"" + testModel + "\",\"messages\":[{\"role\":\"user\",\"content\":\"Test\"}],\"max_tokens\":5}"; + break; + case OPENAI: + default: + testUrl = "https://api.openai.com/v1/chat/completions"; + testModel = "gpt-3.5-turbo"; + jsonBody = "{\"model\":\"" + testModel + "\",\"messages\":[{\"role\":\"user\",\"content\":\"Test\"}],\"max_tokens\":5}"; + break; + } + + // Construire la requête + Request.Builder requestBuilder = new Request.Builder() + .url(testUrl) + .addHeader("Content-Type", "application/json"); + + // Ajouter les headers selon le provider + switch (selectedProvider) { + case OPENAI: + case OPENROUTER: + requestBuilder.addHeader("Authorization", "Bearer " + apiKey); + break; + case ZAI: + requestBuilder.addHeader("x-api-key", apiKey); + requestBuilder.addHeader("anthropic-version", "2023-06-01"); + break; + } + + // Headers spécifiques pour OpenRouter + if (selectedProvider == OpenAIService.AIProvider.OPENROUTER) { + requestBuilder.addHeader("HTTP-Referer", "https://boidelo.app"); + requestBuilder.addHeader("X-Title", "Boidelo"); + } + + Request request = requestBuilder + .post(okhttp3.RequestBody.create(jsonBody, okhttp3.MediaType.parse("application/json"))) .build(); // Exécuter la requête de test client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, IOException e) { - // Gérer les erreurs de requête - e.printStackTrace(); - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), "Échec de la communication avec l'API !", Toast.LENGTH_SHORT).show(); - } + String operation = "Test de connexion API " + selectedProvider.getDisplayName(); + String details = "Échec de connexion lors du test de l'API"; + ErrorHandler.logErrorOnly("JeuxParametres", operation + " - " + details, e); + runOnUiThread(() -> { + String userMessage = "Échec de connexion " + selectedProvider.getDisplayName() + " : " + e.getMessage(); + Toast.makeText(getApplicationContext(), userMessage, Toast.LENGTH_SHORT).show(); }); } @Override public void onResponse(Call call, Response response) throws IOException { - // Vérifier le code de réponse de la requête if (response.isSuccessful()) { - // La clé API est valide et l'API a répondu avec succès - // Vous pouvez effectuer d'autres opérations ici - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), "Communication avec l'API réussie !", Toast.LENGTH_SHORT).show(); - } + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), + "Connexion " + selectedProvider.getDisplayName() + " réussie !", + Toast.LENGTH_SHORT).show(); }); } else { - // La clé API est invalide ou il y a eu une erreur de communication avec l'API - System.out.println("Échec de la communication avec l'API !"); - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), "Échec de la communication avec l'API !", Toast.LENGTH_SHORT).show(); - } + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), + "Erreur " + selectedProvider.getDisplayName() + " (HTTP " + response.code() + ")", + Toast.LENGTH_SHORT).show(); }); } response.close(); @@ -362,7 +470,8 @@ public class JeuxParametres extends AppCompatActivity { intent.putExtra("EXTRA_DURATION_DEFIS", durationDefis); intent.putExtra("EXTRA_OPENAI", openAI); final EditText editText = findViewById(R.id.editTextGPT); - intent.putExtra("EXTRA_KEY_OPENAI",editText.getText().toString() ); + intent.putExtra("EXTRA_KEY_OPENAI", editText.getText().toString()); + intent.putExtra("EXTRA_AI_PROVIDER", selectedProvider.name()); toutlesjoueurs = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR"); intent.putStringArrayListExtra("EXTRA_LIST_JOUEUR", (ArrayList) toutlesjoueurs); diff --git a/app/src/main/java/com/example/boidelov3/Jeuxold.java b/app/src/main/java/com/example/boidelov3/Jeuxold.java deleted file mode 100644 index 547f33e..0000000 --- a/app/src/main/java/com/example/boidelov3/Jeuxold.java +++ /dev/null @@ -1,337 +0,0 @@ -package com.example.boidelov3; - -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatActivity; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - - -public class Jeuxold extends AppCompatActivity { - private List toutlesjoueurs, phraseGPT; - private int nombreQuestions; - private int ajoutGorgees; - boolean openAI; - int ratiOpenai; - String keyOpenai, phraseGPTString; - - // Générateur aléatoire unique pour toute l'activité - private final Random random = new Random(); - - - public Jeuxold() { - //System.out.println("Je suis dans le constructeur jeux"); - } - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_jeux); - //Recuperation des valeurs des activités précédentes - toutlesjoueurs = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR"); - nombreQuestions = getIntent().getIntExtra("EXTRA_NOMBRE_QUESTIONS", 75); - ajoutGorgees = getIntent().getIntExtra("EXTRA_AJOUT_GORGEE", 0); - openAI = getIntent().getBooleanExtra("EXTRA_OPENAI", false); - ratiOpenai = getIntent().getIntExtra("EXTRA_RATIO_OPENAI", 0); - keyOpenai = getIntent().getStringExtra("EXTRA_KEY_OPENAI"); - - - - System.out.println("ACTJeux all player : " + toutlesjoueurs); - System.out.println("ACTJeux nombre de questions : " + nombreQuestions); - System.out.println("ACTJeux ajout de gorgées : " + ajoutGorgees); - System.out.println("ACTJeux openAI : " + openAI); - System.out.println("ACTJeux ratio openAI : " + ratiOpenai); - System.out.println("ACTJeux key openAI : " + keyOpenai); - - - - - - //Parti OpenAI ; keyOpenai ; ratiOpenai, openAI - // 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); -// chatGPTTask.execute(); -// -// } - - //Phrase avec nom ou pas? -/* if(JoueurOuPas()){ - PhraseAvecNom(toutlesjoueurs); - }else{ - PhraseSansNom(); - } - }*/ - } - - - public void handleExtractedMessage(String phraseGPTString) { - // Traitez la réponse extraite ici - System.out.println(phraseGPTString); - // Par exemple, affichez-la dans une TextView ou effectuez une action en fonction de la réponse -} - - public void navigateToJeuxParametres() { - Intent intent = new Intent(Jeuxold.this, JeuxParametres.class); - Toast.makeText(getApplicationContext(), "Échec de la communication avec l'API !", Toast.LENGTH_SHORT).show(); - startActivity(intent); - } - - /*public void PhraseAvecNom(List toutlesjoueurs){ - //System.out.println("Je suis dans phrase avec pseudo"); - List phraseAvecNom = new ArrayList(); - List aleatoirejoueurs = TroisJoueurAleatoire(toutlesjoueurs); - phraseAvecNom.add(ChoixJoueurAleatoire(toutlesjoueurs) + " dois boire " + Gorgeesaleatoire(2, 4)+ " Gorgées"); - phraseAvecNom.add(ChoixJoueurAleatoire((toutlesjoueurs))+ " est le vieux briscard ! Interdiction de montrer tes dents pendant 5 manches"); - phraseAvecNom.add(aleatoirejoueurs.get(0) + " et "+ aleatoirejoueurs.get(1) +" lire le premier SMS qui s'affiche quand on tape désolé dans la barre de recherche. Refusez pour 5 gorgées"); - phraseAvecNom.add( "A tour de rôle, vous avez exactement 3 secondes pour donner un mot en rapport avec le mots dit précédemment. Le joueur qui perd boit "+ Gorgeesaleatoire(2, 4) + " Gorgées! "+ aleatoirejoueurs.get(2)+" tu commences en choisissant un mot."); - phraseAvecNom.add(aleatoirejoueurs.get(0)+ " défie "+ aleatoirejoueurs.get(1) + " au chifoumi ! Le joueur qui gagne distribue 5 Gorgées"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+ " à toi de juger : entre "+aleatoirejoueurs.get(1)+ " et "+ aleatoirejoueurs.get(2) + " qui stresse le plus pour un rien selon toi? Cette personne se détendra en buvant " + Gorgeesaleatoire(3, 5 ) + " Gorgées"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" est dans le futur ! Tu dois parler au futur pendant 4 tours Une gorgée à chaque manque."); - phraseAvecNom.add("Les joueurs de Counter Strike peuvent distribuer" + GorgeesaleatoireAmeliorer(1, 4)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" tu bois autant de gorgées que tu as d'années d'études après le BAC"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+aleatoirejoueurs.get(1)+" ferment leurs yeux ! Ils/Elles doivent deviner la couleur des yeux de l'autre. Si ils/elles se trompent, c'est "+GorgeesaleatoireAmeliorer(2, 4)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" est manchot ! Il/Elle ne peut plus utiliser ses doigts durant 3 tours . Si il/elle s'en sert, il/elle devra boire autant de gorgées qu'il/elle a utilisé de doigts"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+ aleatoirejoueurs.get(1)+" , si vous êtes ensemble dans la vraie vie, vous pouvez distribuer 2 gorgées , autrement buvez-les"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", donne le nombre d'habitants du Tadjikistan ( à 1 000 000 près) ou boit "+GorgeesaleatoireAmeliorer(2, 4)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" a la tourette ! A chaque fois que tu bois une gorgée, tu dois CRIER une insulte. C'est un stade avancé, ça dure 3 tours"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", donne la couleur préférée de "+aleatoirejoueurs.get(1)+" si tu te trompes, c'est 2 gorgées"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" à l'oeil de serpent pendant 5 tours ! Dès qu'un joueur te regarde dans les yeux, il/elle boit. Si personne ne t'a regardé tu bois"+GorgeesaleatoireAmeliorer(5, 9)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+ aleatoirejoueurs.get(1)+"se mesurent ! Le plus petit peut boire"+GorgeesaleatoireAmeliorer(3, 5)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" doit terminer toutes ses phrases par - C'est clair pendant 7 tours"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" distribue"+GorgeesaleatoireAmeliorer(2,5)+" à la personne que tu trouves la mieux foutue"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" distribue"+GorgeesaleatoireAmeliorer(2,5)+" à qui tu veux."); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+aleatoirejoueurs.get(1)+" se défient au 'je te tiens, tu me tiens', le premier qui rit sera une tapette, et devra boire"+GorgeesaleatoireAmeliorer(4,6)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+aleatoirejoueurs.get(1)+"n'ont plus le droit d'utiliser leur téléphone jusqu'à la fin du jeu ! A chaque manque c'est"+GorgeesaleatoireAmeliorer(1,3)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+aleatoirejoueurs.get(1)+ "racontent une anecdote, celui/celle qui sort la plus banale boit "+GorgeesaleatoireAmeliorer(3, 6)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", pour"+GorgeesaleatoireAmeliorer(2,4)+", à qui est ce slogan? Y a pas plus fort. (Vigor)"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", Vrai ou faux? L'eau est bleue car elle reflète le ciel? (Non) Si tu as répondu faux tu devras boire : "+GorgeesaleatoireAmeliorer(2,4)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", Si on te dit Marco? ... Si tu as dit Polo tu bois "+GorgeesaleatoireAmeliorer(1,3)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", Boire un café fait baisser le taux d'alcool? "+GorgeesaleatoireAmeliorer(5, 8)+"en jeu (FAUX)"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" est l'aigris pendant 5 tours ! Dès que tu souris ou rigoles, tu bois "+GorgeesaleatoireAmeliorer(2,3)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" fait un geste, le suivant répète et en ajoute un. Le perdant boit"+GorgeesaleatoireAmeliorer(3,5)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", "+aleatoirejoueurs.get(2)+" et "+aleatoirejoueurs.get(1)+" vont désigner quelqu'un qui doit terminer son verre "); - phraseAvecNom.add("Récitez l'alphabet en énonçant une lettre à tour de rôle. Si "+aleatoirejoueurs.get(0)+" finit son verre avant, cul sec pour tout le monde !"); - phraseAvecNom.add("Si"+aleatoirejoueurs.get(0)+" arrive à finir son verre en moins de 5 secondes, il/elle peut distribuer"+ GorgeesaleatoireAmeliorer(5, 8)); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" et "+ aleatoirejoueurs.get(1)+"sont liés, si l'un boit alors l'autre aussi, et ce pendant 5 tours"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+", "+aleatoirejoueurs.get(2)+" et "+ aleatoirejoueurs.get(1)+"sont liés, si l'un boit alors les autres aussi, et ce pendant 5 tours"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" dit un mot, la personne suivante le répète et en ajoute un nouveau, ainsi de suite jusqu'à ce que quelqu'un se trompe. Le perdant boit autant de gorgées qu'il y a eu de personne avant lui"); - phraseAvecNom.add(aleatoirejoueurs.get(0)+" doit choisir un mot que tout le monde devra dire à chaque fois qu'une personne boit."); - //phraseAvecNom.add(aleatoirejoueurs.get(0)+""); - //phraseAvecNom.add(aleatoirejoueurs.get(0)+""); - //phraseAvecNom.add(aleatoirejoueurs.get(0)+""); - //Affichage : - TextView textView1 = (TextView) findViewById(R.id.textView1); - textView1.setText(Nbaleatoirelist(phraseAvecNom)); - } - public void PhraseSansNom(){ - //System.out.println("Je suis dans phrase sans pseudo"); - List phraseSansNom = new ArrayList(); - //Ajout de defis - phraseSansNom.add("Tout le monde boit "+ Gorgeesaleatoire(1, 2)+" gorgée(s)"); - phraseSansNom.add("Quand l'heure affichera un multiple de 10 (22h, 22h10 ...) le premier à crier \"merde j'ai oublié mon chat\" distribuera " + Gorgeesaleatoire(10, 12)+ " Gorgées"); - phraseSansNom.add("Ceux qui ont dansé aujourd'hui boivent 4 gorgées"); - phraseSansNom.add("Bois "+ Gorgeesaleatoire(2, 6)+ " Gorgées si tu n'as pas ton véritable nom sur insta"); - phraseSansNom.add("Bois "+ Gorgeesaleatoire(2, 3)+ " Gorgées si tu as des photos sur insta."); - phraseSansNom.add("Plutôt ne plus avoir de mains ou de jambes? les perdants boivent "+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles/Ceux qui ont habité dans plus de 3 villes différentes boivent "+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Vive la poésie ! Nos phrases doivent rimer sous peine d'une gorgée"); - phraseSansNom.add("Élisez le joueur le moins drôle d'entre vous, ce dernier boit" + GorgeesaleatoireAmeliorer(1,4 )); - phraseSansNom.add("Élisez le joueur le plus drôle d'entre vous, ce dernier distribue" + GorgeesaleatoireAmeliorer(1,4 )); - phraseSansNom.add("La dernière personne à avoir vomi en soirée distribue" + GorgeesaleatoireAmeliorer(2,4)); - phraseSansNom.add("Les filles peuvent distribuer"+ GorgeesaleatoireAmeliorer(1, 2)); - phraseSansNom.add("Les garçons peuvent distribuer"+ GorgeesaleatoireAmeliorer(1, 2)); - phraseSansNom.add("Toutes celles (ou ceux) qui ont du verni à ongles boivent"+GorgeesaleatoireAmeliorer(1,2)); - phraseSansNom.add("Tous les joueurs célibataires boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Tous ceux qui ont des lunettes boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Le premier joueur qui arrive à mettre son doigt dans le nez d'un autre joueur peut distribuer"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Tous ceux qui ont déjà triché à un examen boivent "+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Plutôt avoir un tapis volant, ou un frigo qui se remplit tout seul ? Votez tous en même temps. La minorité boit "+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Les couples trinquer ensemble "+ GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Le/La plus radin(e) boit"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Le mec qui a le plus gros ventre de bière boit"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Tous ceux qui se sont déjà fait exclure de cours boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Tous ceux qui ont des frères et soeurs boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles et ceux qui ont un Windows phone peuvent distribuer"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles/Ceux qui se sont déjà battus boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celui/Celle qui pèse le plus lourd boit "+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Pour se décoincer, le/la plus timide boit"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Le/La plus jeune boit"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Plutôt avoir du temps ou de l'argent ? Votez tous en même temps. La minorité boit"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles/Ceux qui ont fait des études de L boivent"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Le premier joueur qui en embrasse un autre sur la bouche pourra distribuer"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles et ceux qui jouent de la guitare peuvent distribuer"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Celles et ceux qui jouent du piano peuvent distribuer"+GorgeesaleatoireAmeliorer(1,4)); - phraseSansNom.add("Les gens qui se sont masturbés aujourd'hui peuvent distribuer"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Celui ou celle à la meilleure place boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Celles et ceux qui n'ont jamais trompé leur partenaire (c'est bien) peuvent distribuer"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Celui/Celle avec les vêtements les plus moches boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Celui/Celle qui a les cheveux les plus longs boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("On doit doser son Alcool les yeux fermés"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Plutôt série ou film ? Votez tous en même temps. La minorité boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Élisez le plus débile d'entre vous, ce dernier boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Le premier qui donne un film de - Christopher Nolan - pourra distribuer"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Le premier qui donne un film avec Christian Clavier pourra boire"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Les végans boivent "+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("La fille la plus maquillée boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Celles/Ceux qui ont déjà appelé leur partenaire par le prénom de leurs ex boivent"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("La première personne qui désigne le plus jeune peut distribuer"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Plutôt avoir des connaissances illimitées ou diriger le monde ? Votez tous en même temps. La minorité boit"+GorgeesaleatoireAmeliorer(1, 4)); - phraseSansNom.add("Plutôt n'avoir aucun ami ou ne plus pouvoir utiliser d'appareil électronique ? Votez tous en même temps. La minorité boit"+ GorgeesaleatoireAmeliorer(2, 5)); - phraseSansNom.add("Plutôt vaincre le patrikaka ou la pollution dans le monde? Votez tous en même temps. La minorité boit"+GorgeesaleatoireAmeliorer(1, 2)); - phraseSansNom.add("Jeu du LUTIN : Jusqu'à la fin du jeu. Vous devez enlever le lutin de votre verre pour pouvoir boire et le remettre ensuite sinon vous devez reboire"); - phraseSansNom.add("Celles et ceux qui boivent de la Vodka peuvent distribuer "+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Les joueurs qui ont un A dans leur prénom boivent "+GorgeesaleatoireAmeliorer(3,5)); - phraseSansNom.add("Les joueurs qui ont un P dans le prénom distribuent"+GorgeesaleatoireAmeliorer(1, 3)); - phraseSansNom.add("Le premier joueur à ramener un objet rouge (pas de vêtements) peut distribuer"+GorgeesaleatoireAmeliorer(3,5)); - phraseSansNom.add("Le premier joueur qui dévoile un de ses secrets et que personne autour ne sait peut distribuer"+ GorgeesaleatoireAmeliorer(3, 6)); - phraseSansNom.add("Chaque joueur doit lire à haute voix le dernier SMS qu'il a reçu. Si il/elle refuse, c'est"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Le joueur avec le plus gros cul boit"+ GorgeesaleatoireAmeliorer(2, 6)); - phraseSansNom.add("Celles/Ceux qui ont moins de 20ans boivent"+ GorgeesaleatoireAmeliorer(2, 7)); - phraseSansNom.add("Celui ou celle avec le plus gros appétit sexuel boit"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Ceux/Celles qui fument boivent "+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Celles et ceux qui ont au moins un BAC +3 peuvent distribuer"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Le premier joueur à se lever peut donner"+ GorgeesaleatoireAmeliorer(6, 7)); - phraseSansNom.add("Celles et ceux qui n'ont jamais fait de strip tease boivent"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Le premier joueur à enlever un vêtement pourra distribuer"+ GorgeesaleatoireAmeliorer(5, 7)); - phraseSansNom.add("Jeu des peaux ! Triez vous du joueur le plus bronzé au joueur le moins bronzé. Le plus bronzé prend 1 gorgée, le second 2 gorgées, etc."); - phraseSansNom.add("Tous ceux qui ont déjà uriné dans une piscine boivent"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Celui/Celle avec le plus d'amis sur Facebook boit"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Celui/Celle avec le nom de famille le plus compliqué boit"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Les joueurs qui n'ont pas encore distribué de gorgées boivent"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Plutôt avoir du pouvoir ou de la connaissance ? Votez tous en même temps. La minorité boit"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("le plus gros dalleux avec les filles boit"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Le premier joueur à donner l'heure pourra distribuer"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Celles et ceux qui ont déjà dépenser plus de 2000 euros en un achat peuvent distribuer"+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Le mec le moins courageux boit "+ GorgeesaleatoireAmeliorer(2, 4)); - phraseSansNom.add("Celles/Ceux qui rentre chez eux à la fin de la soirée boivent"+ GorgeesaleatoireAmeliorer(8, 12)); - phraseSansNom.add("Il est désormais interdit de se tutoyer"); - phraseSansNom.add("Toutes les règles existantes sont annulées"); - phraseSansNom.add("Celles et ceux dont le jour d'anniversaire est un nombre impair boivent"+ GorgeesaleatoireAmeliorer(2, 4)); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add(""); - //phraseSansNom.add("");*/ - - - // - - //Affichage : - TextView textView1 = (TextView) findViewById(R.id.textView1); - //textView1.setText(Nbaleatoirelist(phraseSansNom)); - //} - - public int Gorgeesaleatoire(int Min, int Max){ - int offset = ajoutGorgees; - int nbgorgées; - if (Min == 1 && Max == 2){ - nbgorgées = random.nextInt(Max + Min); - }else { - nbgorgées = Min + random.nextInt(Max - Min); - } - if(nbgorgées == 0){ - nbgorgées = 1; - } - nbgorgées = nbgorgées + offset; - return nbgorgées; - } - - public String GorgeesaleatoireAmeliorer(int Min, int Max){ - int offset = ajoutGorgees; - int nbgorgées; - if (Min == 1 && Max == 2){ - nbgorgées = random.nextInt(Max + Min); - }else { - nbgorgées = Min + random.nextInt(Max - Min); - } - if(nbgorgées == 0){ - nbgorgées = 1; - } - nbgorgées = nbgorgées + offset; - - String nbgorgéesstr = " " + Integer.toString(nbgorgées) + " Gorgée(s)"; - return nbgorgéesstr; - } - - public String Nbaleatoirelist(List list){ - return (String) list.get(random.nextInt(list.size())); - } - - public int Nbaleatoire(){ - int Max = 100; - int Min = 0; - return random.nextInt(Max - Min); - } - public boolean JoueurOuPas(){ - boolean TrueFalse; - int nbaleatoire = Nbaleatoire(); - int pourcentage = 40; - //System.out.println(nbaleatoire); - if(nbaleatoire >= pourcentage){ - TrueFalse = false;} - else{ - TrueFalse = true; - } - //System.out.println(TrueFalse); - return TrueFalse; - } - public List TroisJoueurAleatoire(List toutlesjoueurs){ - List listJoueur = new ArrayList(); - while (listJoueur.size() < 3){ - String joueur = (String) toutlesjoueurs.get(random.nextInt(toutlesjoueurs.size())); - if (!listJoueur.contains(joueur)) { - listJoueur.add(joueur); - } - } - return listJoueur; - } - - public String ChoixJoueurAleatoire(List toutlesjoueurs){ - return (String) toutlesjoueurs.get(random.nextInt(toutlesjoueurs.size())); - } - public void OnClickButton1(View v){ - finish(); - startActivity(getIntent()); - - } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // Votre code pour gérer les modifications d'orientation ici - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/example/boidelov3/MainActivity.java b/app/src/main/java/com/example/boidelov3/MainActivity.java index 84677ad..8aa3c71 100644 --- a/app/src/main/java/com/example/boidelov3/MainActivity.java +++ b/app/src/main/java/com/example/boidelov3/MainActivity.java @@ -159,19 +159,32 @@ public class MainActivity extends AppCompatActivity { // Ajouter l'EditText au TextInputLayout textInputLayout.addView(newEditText); - // Créer le bouton de suppression - taille réduite + // Créer le bouton de suppression - style élégant avec fond circulaire ImageButton deleteButton = new ImageButton(this); - int dp36 = (int) (36 * getResources().getDisplayMetrics().density); - FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(dp36, dp36); - buttonParams.setMargins(0, 4, 10, 4); // Plus d'espace à droite (10dp au lieu de 6) + int dp40 = (int) (40 * getResources().getDisplayMetrics().density); + FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(dp40, dp40); + buttonParams.setMargins(0, 0, 8, 0); buttonParams.gravity = android.view.Gravity.END | android.view.Gravity.CENTER_VERTICAL; deleteButton.setLayoutParams(buttonParams); + + // Utiliser l'icône de poubelle Material (ic_menu_delete est une croix) deleteButton.setImageResource(android.R.drawable.ic_menu_delete); - int errorColor = androidx.core.content.ContextCompat.getColor(this, R.color.error); - deleteButton.setColorFilter(errorColor); + deleteButton.setColorFilter(androidx.core.content.ContextCompat.getColor(this, R.color.text_secondary)); deleteButton.setScaleType(ImageButton.ScaleType.CENTER_INSIDE); - deleteButton.setBackgroundColor(android.graphics.Color.TRANSPARENT); // Fond transparent - deleteButton.setPadding(4, 4, 4, 4); // Padding interne réduit + + // Fond circulaire blanc avec bordure fine + deleteButton.setBackgroundColor(androidx.core.content.ContextCompat.getColor(this, R.color.white)); + deleteButton.setPadding(8, 8, 8, 8); + + // Arrondir le bouton en cercle via un OutlineProvider + deleteButton.setOutlineProvider(new android.view.ViewOutlineProvider() { + @Override + public void getOutline(android.view.View view, android.graphics.Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }); + deleteButton.setClipToOutline(true); + deleteButton.setContentDescription("Supprimer ce joueur"); // Configuration du bouton de suppression avec animation @@ -263,6 +276,22 @@ public class MainActivity extends AppCompatActivity { } public void openParametres(){ + // Vérifier s'il y a des noms en double (insensible à la casse) + List nomsNormauxes = new ArrayList<>(); + for (String nom : toutlesjoueurs) { + String nomNormalise = nom.trim().toLowerCase(); + if (nomsNormauxes.contains(nomNormalise)) { + // Nom en double détecté + Context context = getApplicationContext(); + CharSequence text = "Erreur : Le nom \"" + nom + "\" est utilisé plusieurs fois. Chaque joueur doit avoir un nom unique."; + int duration = Toast.LENGTH_LONG; + Toast toast = Toast.makeText(context, text, duration); + toast.show(); + return; // Ne pas continuer + } + nomsNormauxes.add(nomNormalise); + } + //enregistrement des joueurs dans les shared preferences Joueurs SharedPreferences sharedPreferences = getSharedPreferences("Joueurs", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); diff --git a/app/src/main/java/com/example/boidelov3/OpenAIService.java b/app/src/main/java/com/example/boidelov3/OpenAIService.java new file mode 100644 index 0000000..0a49b9c --- /dev/null +++ b/app/src/main/java/com/example/boidelov3/OpenAIService.java @@ -0,0 +1,389 @@ +package com.example.boidelov3; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * Service pour générer des questions via différentes IA (OpenAI, OpenRouter, Z.ai) + * Utilise OkHttp pour les requêtes réseau asynchrones + */ +public class OpenAIService { + private static final String TAG = "OpenAIService"; + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + // API URLs pour chaque provider + private static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions"; + private static final String OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"; + + // Modèles par défaut pour chaque provider + private static final String OPENAI_MODEL = "gpt-3.5-turbo"; + private static final String OPENROUTER_MODEL = "openai/gpt-3.5-turbo"; + private static final String ZAI_MODEL = "claude-3-5-sonnet"; + + private OkHttpClient client; + private String apiKey; + private AIProvider provider; + private String model; + private Handler mainHandler; + private Random random; + + /** + * Enum pour les différents fournisseurs d'IA + */ + public enum AIProvider { + OPENAI("OpenAI", OPENAI_API_URL, OPENAI_MODEL), + OPENROUTER("OpenRouter", OPENROUTER_API_URL, OPENROUTER_MODEL), + ZAI("Z.ai", "https://api.z.ai/v1/messages", ZAI_MODEL); + + private final String displayName; + private final String apiUrl; + private final String defaultModel; + + AIProvider(String displayName, String apiUrl, String defaultModel) { + this.displayName = displayName; + this.apiUrl = apiUrl; + this.defaultModel = defaultModel; + } + + public String getDisplayName() { + return displayName; + } + + public String getApiUrl() { + return apiUrl; + } + + public String getDefaultModel() { + return defaultModel; + } + } + + /** + * Constructeur avec provider par défaut (OpenAI) + */ + public OpenAIService(String apiKey) { + this(apiKey, AIProvider.OPENAI); + } + + /** + * Constructeur avec choix du provider + * Valide la clé API avant de l'utiliser + */ + public OpenAIService(String apiKey, AIProvider provider) { + // Valider la clé API avant utilisation + if (!validateApiKey(apiKey, provider)) { + throw new IllegalArgumentException("Clé API invalide pour " + provider.getDisplayName()); + } + + this.apiKey = apiKey; + this.provider = provider; + this.model = provider.getDefaultModel(); + this.client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.mainHandler = new Handler(Looper.getMainLooper()); + this.random = new Random(); + } + + /** + * Constructeur avec choix du provider et du modèle + * Valide la clé API avant de l'utiliser + */ + public OpenAIService(String apiKey, AIProvider provider, String model) { + this(apiKey, provider); + this.model = model; + } + + /** + * Valide une clé API selon le provider + * Vérifie que la clé n'est pas null, vide, et a un format valide + * + * @param key La clé API à valider + * @param provider Le provider d'IA + * @return true si la clé est valide, false sinon + */ + public static boolean validateApiKey(String key, AIProvider provider) { + // Vérification basique : la clé ne doit pas être null ou vide + if (key == null || key.trim().isEmpty()) { + Log.w(TAG, "Clé API null ou vide pour " + provider.getDisplayName()); + return false; + } + + String trimmedKey = key.trim(); + + // Vérification du format selon le provider + switch (provider) { + case OPENAI: + // Les clés OpenAI commencent par "sk-" + if (!trimmedKey.startsWith("sk-")) { + Log.w(TAG, "Clé OpenAI invalide : doit commencer par 'sk-'"); + return false; + } + if (trimmedKey.length() < 20) { + Log.w(TAG, "Clé OpenAI trop courte"); + return false; + } + break; + + case OPENROUTER: + // Les clés OpenRouter commencent par "sk-or-" + if (!trimmedKey.startsWith("sk-or-")) { + Log.w(TAG, "Clé OpenRouter invalide : doit commencer par 'sk-or-'"); + return false; + } + if (trimmedKey.length() < 20) { + Log.w(TAG, "Clé OpenRouter trop courte"); + return false; + } + break; + + case ZAI: + // Les clés Z.ai/Anthropic commencent par "sk-ant-" + if (!trimmedKey.startsWith("sk-ant-")) { + Log.w(TAG, "Clé Z.ai invalide : doit commencer par 'sk-ant-'"); + return false; + } + if (trimmedKey.length() < 20) { + Log.w(TAG, "Clé Z.ai trop courte"); + return false; + } + break; + + default: + Log.w(TAG, "Provider inconnu : " + provider); + return false; + } + + Log.i(TAG, "Clé API validée pour " + provider.getDisplayName()); + return true; + } + + /** + * Valide une clé API avec le provider par défaut (OpenAI) + * + * @param key La clé API à valider + * @return true si la clé est valide + */ + public static boolean validateApiKey(String key) { + return validateApiKey(key, AIProvider.OPENAI); + } + + /** + * Change le provider + */ + public void setProvider(AIProvider provider) { + this.provider = provider; + this.model = provider.getDefaultModel(); + } + + /** + * Change le modèle + */ + public void setModel(String model) { + this.model = model; + } + + /** + * Génère une question via l'API configurée + */ + public void generateQuestion(List players, int ajoutGorgees, OpenAICallback callback) { + String prompt = buildPrompt(players, ajoutGorgees); + + try { + boolean isAnthropicFormat = (provider == AIProvider.ZAI); + String requestBody; + String responseParser; + + if (isAnthropicFormat) { + // Format Anthropic (Claude/Z.ai) + JSONObject body = new JSONObject(); + body.put("model", model); + body.put("max_tokens", 150); + body.put("temperature", 0.8); + + JSONArray messages = new JSONArray(); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", "user"); + userMessage.put("content", prompt); + messages.put(userMessage); + body.put("messages", messages); + + requestBody = body.toString(); + responseParser = "anthropic"; + } else { + // Format OpenAI-compatible + JSONObject body = new JSONObject(); + body.put("model", model); + body.put("max_tokens", 150); + body.put("temperature", 0.8); + + JSONArray messages = new JSONArray(); + JSONObject userMessage = new JSONObject(); + userMessage.put("role", "user"); + userMessage.put("content", prompt); + messages.put(userMessage); + body.put("messages", messages); + + requestBody = body.toString(); + responseParser = "openai"; + } + + // Construire la requête avec l'URL appropriée + Request.Builder requestBuilder = new Request.Builder() + .url(provider.getApiUrl()) + .post(RequestBody.create(requestBody, JSON)); + + // Ajouter les headers selon le provider + switch (provider) { + case OPENAI: + requestBuilder.addHeader("Authorization", "Bearer " + apiKey); + break; + case OPENROUTER: + requestBuilder.addHeader("Authorization", "Bearer " + apiKey); + requestBuilder.addHeader("HTTP-Referer", "https://boidelo.app"); + requestBuilder.addHeader("X-Title", "Boidelo"); + break; + case ZAI: + requestBuilder.addHeader("x-api-key", apiKey); + requestBuilder.addHeader("anthropic-version", "2023-06-01"); + break; + } + + Request request = requestBuilder.build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + mainHandler.post(() -> callback.onError("Erreur de connexion: " + e.getMessage())); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + mainHandler.post(() -> callback.onError("Erreur API " + provider.getDisplayName() + ": " + response.code())); + return; + } + + String responseData = response.body().string(); + String generatedQuestion = parseResponse(responseData, responseParser); + + mainHandler.post(() -> callback.onSuccess(generatedQuestion)); + } + }); + + } catch (JSONException e) { + mainHandler.post(() -> callback.onError("Erreur de formatage: " + e.getMessage())); + } + } + + /** + * Construit le prompt pour l'IA + */ + private String buildPrompt(List players, int ajoutGorgees) { + StringBuilder sb = new StringBuilder(); + sb.append("Tu es un générateur de questions pour un jeu d'alcool en français. "); + sb.append("Génère UNE SEULE question dans le style suivant:\n\n"); + + sb.append("Exemples:\n"); + sb.append("- Ceux qui ont un tatouage\n"); + sb.append("- Le/La plus drôle du groupe\n"); + sb.append("- doit deviner ce que a mangé aujourd'hui\n"); + sb.append("- Ceux qui écoutent du rock\n"); + sb.append("- Plutôt avoir le pouvoir ou la connaissance ? Votez tous. La minorité boit.\n"); + + if (players != null && !players.isEmpty()) { + sb.append("\nJoueurs présents: "); + for (int i = 0; i < players.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(players.get(i)); + } + } + + if (ajoutGorgees > 0) { + sb.append("\nAjout de gorgées: +").append(ajoutGorgees).append(" gorgée(s)"); + } + + sb.append("\n\nIMPORTANT: "); + sb.append("Génère uniquement la question, sans explication. "); + sb.append("Utilise , , pour désigner des joueurs aléatoires si nécessaire. "); + sb.append("Ne génère PAS de questions avec (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 "); + q.setVariante(Arrays.asList("A", "B")); + + QuestionCategory.Category category = QuestionCategory.detectCategory(q); + assertEquals("DEFI_MANCHES should have priority over VARIANTE", + QuestionCategory.Category.DEFI_MANCHES, category); + } + + @Test + public void testCategoryEnum_allFieldsAccessible() { + QuestionCategory.Category ciblage = QuestionCategory.Category.CIBLAGE; + assertEquals("Ciblage", ciblage.getName()); + assertEquals("Questions qui ciblent un groupe spécifique", ciblage.getDescription()); + assertTrue("Color should be positive", ciblage.getColor() > 0); + } +} diff --git a/app/src/test/java/com/example/boidelov3/game/GameEngineTest.java b/app/src/test/java/com/example/boidelov3/game/GameEngineTest.java index 6d0c397..cf4a84c 100644 --- a/app/src/test/java/com/example/boidelov3/game/GameEngineTest.java +++ b/app/src/test/java/com/example/boidelov3/game/GameEngineTest.java @@ -201,4 +201,186 @@ public class GameEngineTest { } return false; } + + // Tests supplémentaires pour une meilleure couverture + + @Test + public void testSelectRandomPlayers_withSinglePlayer() { + List singlePlayer = Arrays.asList("Alice"); + List selected = gameEngine.selectRandomPlayers(singlePlayer, 1); + + assertEquals("Should select 1 player", 1, selected.size()); + assertEquals("Should be Alice", "Alice", selected.get(0)); + } + + @Test + public void testProcessQuestion_withBothRecoisAndDistribution() { + Question question = createQuestion("Test"); + question.setRecois(true); + question.setDistribution(true); + question.setGorger(2); + + GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0); + String text = processed.question.getQuestion(); + + // Should contain either "bois" or "distribue" (random choice) + boolean containsBois = text.contains("bois"); + boolean containsDistribue = text.contains("distribue"); + assertTrue("Should contain either 'bois' or 'distribue'", containsBois || containsDistribue); + } + + @Test + public void testProcessQuestion_withNoGorgeesFlags() { + Question question = createQuestion("Question sans gorgées"); + question.setRecois(false); + question.setDistribution(false); + + GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0); + String text = processed.question.getQuestion(); + + assertFalse("Should not contain 'bois'", text.contains("bois")); + assertFalse("Should not contain 'distribue'", text.contains("distribue")); + assertFalse("Should not contain 'gorgée'", text.contains("gorgée")); + } + + @Test + public void testUpdateManches_withNoActiveManches() { + GameEngine.MancheState state = gameEngine.updateManches(); + + assertNull("Active manche should be null", state.activeManche); + assertFalse("Should not have manche", state.hasManche); + assertNull("End message should be null", state.endMessage); + } + + @Test + public void testProcessQuestion_mancheDecrementsCorrectly() { + Question question = createQuestion("Défi "); + question.setArret("Fin !"); + + gameEngine.processQuestion(question, players, 0); + + // Get initial state + GameEngine.MancheState state1 = gameEngine.updateManches(); + int count1 = state1.activeManche.getManchesRestantes(); + + // Update again + GameEngine.MancheState state2 = gameEngine.updateManches(); + int count2 = state2.activeManche.getManchesRestantes(); + + assertEquals("Manche should decrement by 1", count1 - 1, count2); + } + + @Test + public void testProcessQuestion_mancheFinishes_returnsEndMessage() { + Question question = createQuestion("Défi "); + question.setArret("Bravo !"); + + gameEngine.processQuestion(question, players, 0); + + // Update until manche ends (1 left -> 0) + GameEngine.MancheState state; + do { + state = gameEngine.updateManches(); + } while (state.hasManche); + + assertNotNull("Should have end message", state.endMessage); + assertTrue("End message should contain stop message", + state.endMessage.contains("Fin de défi!") || state.endMessage.contains("Bravo !")); + } + + @Test + public void testGetActiveManchesCount_incrementsWithManches() { + assertEquals("Initial count should be 0", 0, gameEngine.getActiveManchesCount()); + + Question q1 = createQuestion("Défi 1 "); + q1.setArret("Fin 1"); + gameEngine.processQuestion(q1, players, 0); + + assertEquals("Count should be 1", 1, gameEngine.getActiveManchesCount()); + + Question q2 = createQuestion("Défi 2 "); + q2.setArret("Fin 2"); + gameEngine.processQuestion(q2, players, 0); + + assertEquals("Count should be 2", 2, gameEngine.getActiveManchesCount()); + } + + @Test + public void testClearManches_afterMultipleManches() { + Question q1 = createQuestion("Défi 1 "); + q1.setArret("Fin 1"); + gameEngine.processQuestion(q1, players, 0); + + Question q2 = createQuestion("Défi 2 "); + q2.setArret("Fin 2"); + gameEngine.processQuestion(q2, players, 0); + + assertTrue("Should have active manches", gameEngine.hasActiveManche()); + + gameEngine.clearManches(); + + assertFalse("Should have no active manches", gameEngine.hasActiveManche()); + assertEquals("Count should be 0", 0, gameEngine.getActiveManchesCount()); + } + + @Test + public void testProcessQuestion_preservesOriginalQuestion() { + Question original = createQuestion(" bois 2 gorgées"); + original.setGorger(2); + original.setRecois(true); + + String originalText = original.getQuestion(); + + gameEngine.processQuestion(original, players, 0); + + assertEquals("Original question should be unchanged", originalText, original.getQuestion()); + } + + @Test + public void testProcessQuestion_withEmptyVarianteList() { + Question question = createQuestion("Question "); + question.setVariante(Arrays.asList()); + + GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0); + String text = processed.question.getQuestion(); + + // Should not replace variante if list is empty + assertTrue("Should still contain tag", text.contains("")); + } + + @Test + public void testSelectRandomPlayers_returnsSameSizeWhenRequestedMore() { + List smallList = Arrays.asList("A", "B"); + List selected = gameEngine.selectRandomPlayers(smallList, 5); + + assertEquals("Should return max available", 2, selected.size()); + } + + @Test + public void testProcessQuestion_mancheWithArretNull() { + Question question = createQuestion("Défi "); + question.setArret(null); + + GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0); + + assertTrue("Should be a manche", processed.isManche); + assertNotNull("Should have default end message", processed.question.getArretMessageManche()); + } + + @Test + public void testProcessQuestion_withZeroAddedGorgees() { + Question question = createQuestion("Test"); + question.setDistribution(true); + question.setGorger(3); + + GameEngine.ProcessedQuestion processed = gameEngine.processQuestion(question, players, 0); + String text = processed.question.getQuestion(); + + assertTrue("Should contain base gorgées (3)", text.contains("3")); + } + + @Test + public void testHasActiveManche_initiallyFalse() { + assertFalse("Should not have active manche initially", gameEngine.hasActiveManche()); + } }