feat: Ajout du jeu Papelito, améliorations UX et corrections de bugs

Nouveau jeu:
- Ajout du jeu Papelito (Undercover) avec flow complet
- Configuration des joueurs, temps de discussion, votes
- Système d'élimination et gestion des égalités
- Interface Material Design avec cartes et dialogues

Corrections de bugs critiques:
- Fix crash Papelito au lancement (MaterialSwitch vs Switch)
- Fix crash lors des votes nuls (égalité entre joueurs)
- Fix crash fin de partie lors du retour (navigation vers hub)
- Fix visibilité texte questions (couleur dynamique)
- Fix compteur tours défis invisible (blanc sur blanc)
- Fix icone question manquante pendant défis

Améliorations UX Boidelo Classic:
- Harmonisation des couleurs dynamiques (toolbar, bouton)
- Bouton de réglages maintenant visible (MaterialButton)
- Conteneur IA se rétracte quand désactivé
- Meilleure gestion des couleurs selon catégorie
- Fix délai entre manches pour affichage message fin

Améliorations techniques:
- Mise à jour CLAUDE.md avec architecture Papelito
- Amélioration tests unitaires (GameEngine, PlayerStats, QuestionCategory)
- Standardisation des clés Intent entre activités
- Nettoyage code mort (méthodes non utilisées)

Tests:
- 302 tests unitaires passants
- Couverture GameEngine, PlayerStats, QuestionCategory
- Tests Papelito (game logic, player management)
- Tests Game89 (challenges, players)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
feldenr
2026-01-16 12:05:29 +01:00
co-authored by Claude
parent 1b3d67526d
commit c803469643
42 changed files with 6549 additions and 201 deletions
@@ -82,7 +82,12 @@ 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");
// Essayer avec les deux clés possibles (PLAYER_STATS ou EXTRA_PLAYER_STATS)
playerStatsList = getIntent().getParcelableArrayListExtra("PLAYER_STATS");
if (playerStatsList == null) {
playerStatsList = getIntent().getParcelableArrayListExtra("EXTRA_PLAYER_STATS");
}
// Si pas de données, utiliser les SharedPreferences
if (questionsPlayed == 0) {
@@ -177,20 +182,20 @@ public class EndGameActivity extends AppCompatActivity {
}
/**
* Retourne à l'écran d'accueil
* Retourne à l'écran d'accueil (hub de jeux)
*/
private void goToHome() {
Intent intent = new Intent(this, MainActivity.class);
Intent intent = new Intent(this, com.example.boidelov3.hub.GameSelectionActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
/**
* Relance une nouvelle partie
* Relance une nouvelle partie (retourne au hub)
*/
private void replay() {
Intent intent = new Intent(this, MainActivity.class);
Intent intent = new Intent(this, com.example.boidelov3.hub.GameSelectionActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
@@ -87,9 +87,9 @@ public class Jeux extends AppCompatActivity {
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 MIN_DEFI_ROUNDS = 4; // Minimum 4 manches pour les défis
private static final int MAX_DEFI_ROUNDS_RANDOM = 6; // Max 6 tours aléatoires (4-10 tours au total)
private static final int MIN_MANCHES_COUNT = 4;
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
@@ -118,11 +118,11 @@ public class JeuxParametres extends AppCompatActivity {
int initialQuestions = 50;
int initialGorgees = 0;
int initialRatio = 8;
int initialDuration = 0; // 0 pour avoir 3-8 tours par défaut (MIN_DEFI_ROUNDS=3)
int initialDuration = 0; // 0 pour avoir 4-10 tours par défaut (MIN_DEFI_ROUNDS=4)
questionCountValue.setText(String.valueOf(initialQuestions));
gorgeesValue.setText(String.valueOf(initialGorgees));
durationValue.setText("0"); // Afficher 0 par défaut pour avoir 3-8 tours
durationValue.setText("0"); // Afficher 0 par défaut pour avoir 4-10 tours
textView5.setText("Palier : Grosse merde");
textViewRatioGen.setText("Ratio BDD/OPENAI : 1/" + initialRatio);
@@ -150,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 pour avoir 3-8 tours (MIN_DEFI_ROUNDS=3)
seekBarDuration.setProgress(0); // Valeur par défaut à 0 pour avoir 4-10 tours (MIN_DEFI_ROUNDS=4)
// Configuration des listeners pour les seekBars
seekBar1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@@ -11,5 +11,15 @@ public class Questions {
return questions;
}
// autres getters et setters...
public void setQuestions(List<Question> questions) {
this.questions = questions;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}
@@ -41,7 +41,7 @@ public class GameEngine {
// Gérer les manches
if (questionText.contains("<manches>")) {
int manchesCount = random.nextInt(10) + 5;
int manchesCount = random.nextInt(7) + 4; // 4-10 manches
questionText = questionText.replace("<manches>", String.valueOf(manchesCount));
// Créer une copie de la question pour la manche active
@@ -1,6 +1,7 @@
package com.example.boidelov3.games.boideloclassic;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.text.Html;
import android.view.Gravity;
@@ -12,6 +13,8 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.google.android.material.appbar.MaterialToolbar;
import com.example.boidelov3.BoideloAnimationUtils;
import com.example.boidelov3.EndGameActivity;
@@ -39,12 +42,13 @@ import java.util.Random;
public class BoideloClassicGameActivity extends AppCompatActivity {
// UI Components
private MaterialToolbar toolbar;
private TextView questionTextView;
private TextView progressTextView;
private TextView mancheCounterTextView;
private TextView mancheQuestionText;
private ProgressBar progressBar;
private View suivantButton;
private com.google.android.material.button.MaterialButton suivantButton;
private View skipButton;
private View questionIndicator;
private View indicatorIcon;
@@ -85,9 +89,9 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
private int questionsSinceLastAI = 0;
// Constants
private static final int MIN_DEFI_ROUNDS = 3;
private static final int MAX_DEFI_ROUNDS_RANDOM = 15;
private static final int MIN_MANCHES_COUNT = 5;
private static final int MIN_DEFI_ROUNDS = 4; // Minimum 4 manches
private static final int MAX_DEFI_ROUNDS_RANDOM = 6; // Max 6 tours aléatoires (4-10 tours au total)
private static final int MIN_MANCHES_COUNT = 4; // Minimum 4 manches
private static final int PREGENERATED_AI_QUESTIONS = 10;
private static final int MIN_AI_QUESTION_STOCK = 3;
private static final int MIN_AI_GORGEE = 1;
@@ -102,21 +106,37 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boidelo_classic_game);
// Configure la toolbar avec un bouton retour
MaterialToolbar toolbar = findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
// Récupère les joueurs depuis l'intent
// Récupère les joueurs et les paramètres depuis l'intent
Intent intent = getIntent();
toutlesjoueurs = intent.getStringArrayListExtra("PLAYERS");
// Récupérer les paramètres de jeu
if (intent.hasExtra("EXTRA_NOMBRE_QUESTIONS")) {
nombreQuestions = intent.getIntExtra("EXTRA_NOMBRE_QUESTIONS", 20);
}
if (intent.hasExtra("EXTRA_AJOUT_GORGEE")) {
ajoutGorgees = intent.getIntExtra("EXTRA_AJOUT_GORGEE", 1);
}
if (intent.hasExtra("EXTRA_OPENAI")) {
openAI = intent.getBooleanExtra("EXTRA_OPENAI", false);
}
if (intent.hasExtra("EXTRA_RATIO_OPENAI")) {
ratiOpenai = intent.getIntExtra("EXTRA_RATIO_OPENAI", 5);
}
if (intent.hasExtra("EXTRA_KEY_OPENAI")) {
keyOpenai = intent.getStringExtra("EXTRA_KEY_OPENAI");
}
if (intent.hasExtra("EXTRA_DURATION_DEFIS")) {
durationDefis = intent.getIntExtra("EXTRA_DURATION_DEFIS", 0);
}
initViews();
initServices();
loadQuestions();
initializePlayerStats();
setupProgressBar();
setupButtonListeners();
// Affiche la première question
displayNewQuestion();
}
@@ -125,6 +145,9 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
* Initialise les vues de l'activité
*/
private void initViews() {
toolbar = findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
questionTextView = findViewById(R.id.questionTextView);
progressTextView = findViewById(R.id.progressTextView);
mancheCounterTextView = findViewById(R.id.mancheCounterTextView);
@@ -485,15 +508,123 @@ public class BoideloClassicGameActivity extends AppCompatActivity {
int categoryColor = QuestionCategory.getColorForCategory(category);
BoideloAnimationUtils.animateBackgroundColor(rootLayout, categoryColor, 300);
// N'afficher l'indicateur que si un défi n'est PAS en cours
if (questionsAvecManches.isEmpty()) {
String indicatorText = getCategoryQuestionIndicator(category, question);
if (!indicatorText.isEmpty()) {
showQuestionIndicatorWithEmoji(indicatorText);
}
// Harmoniser les couleurs de la toolbar et du bouton
harmonizeUiColors(categoryColor);
// Afficher l'indicateur pour toutes les questions (y compris pendant les défis)
String indicatorText = getCategoryQuestionIndicator(category, question);
if (!indicatorText.isEmpty()) {
showQuestionIndicatorWithEmoji(indicatorText);
}
}
/**
* Harmonise les couleurs de la toolbar et du bouton avec la couleur de fond
*/
private void harmonizeUiColors(int baseColor) {
// Créer une teinte plus foncée pour la toolbar (25% plus foncée pour plus de contraste)
int toolbarColor = darkenColor(baseColor, 0.25f);
// Créer une teinte plus foncée pour le bouton (15% plus foncée pour être plus visible)
int buttonColor = darkenColor(baseColor, 0.15f);
// Créer une teinte très foncée pour la progressBar et texte (30% plus foncée)
int accentColor = darkenColor(baseColor, 0.3f);
// Déterminer la couleur du texte selon la luminosité du fond
int textColor = isColorDark(baseColor) ? Color.WHITE : Color.BLACK;
int lighterTextColor = isColorDark(baseColor) ?
lightenColor(Color.WHITE, 0.3f) : // Gris clair pour fond sombre
darkenColor(Color.BLACK, 0.3f); // Gris foncé pour fond clair
// Appliquer la couleur à la toolbar
toolbar.setBackgroundColor(toolbarColor);
// Appliquer la couleur au bouton suivant avec plus de contraste
if (suivantButton != null) {
suivantButton.setBackgroundTintList(
ContextCompat.getColorStateList(this, android.R.color.transparent)
);
suivantButton.setBackgroundColor(buttonColor);
// Texte toujours blanc pour le bouton pour plus de lisibilité
suivantButton.setTextColor(Color.WHITE);
}
// Appliquer la couleur à la progressBar
if (progressBar != null) {
progressBar.setProgressTintList(
android.content.res.ColorStateList.valueOf(accentColor)
);
}
// Appliquer la couleur aux textes
if (progressTextView != null) {
progressTextView.setTextColor(lighterTextColor);
}
// Pour le compteur de manches, utiliser une couleur foncée car il est dans une carte (bg_card)
if (mancheCounterTextView != null) {
// Toujours utiliser du gris foncé pour le compteur (il est sur fond de carte)
mancheCounterTextView.setTextColor(Color.parseColor("#424242"));
}
if (mancheQuestionText != null) {
mancheQuestionText.setTextColor(textColor);
}
// Adapter aussi la couleur de l'indicateur (icône et texte)
if (indicatorText != null) {
indicatorText.setTextColor(textColor);
}
if (indicatorIcon != null && indicatorIcon instanceof android.widget.ImageView) {
android.widget.ImageView imageView = (android.widget.ImageView) indicatorIcon;
imageView.setColorFilter(textColor, android.graphics.PorterDuff.Mode.SRC_IN);
}
// NE PAS changer la couleur du texte de la question - garder la couleur par défaut
}
/**
* Assombrit une couleur d'un certain pourcentage
*/
private int darkenColor(int color, float percent) {
int alpha = Color.alpha(color);
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
red = Math.max((int) (red * (1 - percent)), 0);
green = Math.max((int) (green * (1 - percent)), 0);
blue = Math.max((int) (blue * (1 - percent)), 0);
return Color.argb(alpha, red, green, blue);
}
/**
* Éclaircit une couleur d'un certain pourcentage
*/
private int lightenColor(int color, float percent) {
int alpha = Color.alpha(color);
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
red = Math.min((int) (red + (255 - red) * percent), 255);
green = Math.min((int) (green + (255 - green) * percent), 255);
blue = Math.min((int) (blue + (255 - blue) * percent), 255);
return Color.argb(alpha, red, green, blue);
}
/**
* Détermine si une couleur est foncée (pour choisir la couleur du texte)
*/
private boolean isColorDark(int color) {
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return darkness >= 0.5;
}
/**
* Retourne l'emoji associé à une catégorie
*/
@@ -1,24 +1,62 @@
package com.example.boidelov3.games.boideloclassic;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
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;
import android.widget.LinearLayout;
import com.google.android.material.switchmaterial.SwitchMaterial;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.example.boidelov3.OpenAIService;
import com.example.boidelov3.R;
import com.example.boidelov3.utils.ErrorHandler;
import java.io.IOException;
import java.util.ArrayList;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* BoideloClassicParamsActivity - Écran de paramètres pour Boidelo Classic
*
* Cette activité permet de configurer les paramètres du jeu :
* - Nombre de questions
* - Nombre de gorgées
* - Activation/désactivation de l'IA
* - Durée des défis
*
* C'est une version refactorisée de l'ancienne JeuxParametres.java
*/
public class BoideloClassicParamsActivity 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 editTextKeyGPT;
private AutoCompleteTextView autoCompleteProvider;
private com.google.android.material.card.MaterialCardView openaiCard;
private LinearLayout openaiContentLayout;
private String keyGPT;
private OpenAIService.AIProvider selectedProvider = OpenAIService.AIProvider.OPENAI;
private int nbQuestions;
private ArrayList<String> toutlesjoueurs;
@Override
protected void onCreate(Bundle savedInstanceState) {
toutlesjoueurs = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_boidelo_classic_params);
@@ -28,37 +66,431 @@ public class BoideloClassicParamsActivity extends AppCompatActivity {
getSupportActionBar().setTitle(R.string.parameters);
}
initViews();
setupListeners();
loadCurrentSettings();
// Initialisation des vues
seekBar1 = findViewById(R.id.seekBar1);
seekBar2 = findViewById(R.id.seekBar2);
seekBar3 = findViewById(R.id.seekBar3);
seekBarDuration = findViewById(R.id.seekBarDuration);
textView1 = findViewById(R.id.textView1);
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);
openaiCard = findViewById(R.id.openaiCard);
// Récupérer le LinearLayout qui contient tous les éléments de la carte IA
openaiContentLayout = findViewById(R.id.openaiCardContent);
// Configuration du dropdown pour le provider IA
String[] providers = new String[]{
OpenAIService.AIProvider.OPENAI.getDisplayName(),
OpenAIService.AIProvider.OPENROUTER.getDisplayName(),
OpenAIService.AIProvider.ZAI.getDisplayName()
};
ArrayAdapter<String> 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; // 0 pour avoir 4-10 tours par défaut (MIN_DEFI_ROUNDS=4)
questionCountValue.setText(String.valueOf(initialQuestions));
gorgeesValue.setText(String.valueOf(initialGorgees));
durationValue.setText("0"); // Afficher 0 par défaut pour avoir 4-10 tours
textView5.setText("Palier : Grosse merde");
textViewRatioGen.setText("Ratio BDD/OPENAI : 1/" + initialRatio);
// Configuration de la seekBar1
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
seekBar1.setMin(20);
}
seekBar1.setMax(150);
seekBar1.setProgress(50);
// Configuration de la seekBar2
seekBar2.setMax(20);
seekBar2.setProgress(0);
// Configuration de la seekBar3
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
seekBar2.setMin(0);
seekBar3.setMin(1);
}
seekBar3.setMax(15);
seekBar3.setProgress(8);
// Configuration de la seekBarDuration (permet valeurs négatives pour offset)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
seekBarDuration.setMin(-5); // Permet un offset négatif jusqu'à -5
}
seekBarDuration.setMax(15);
seekBarDuration.setProgress(0); // Valeur par défaut à 0 pour avoir 4-10 tours (MIN_DEFI_ROUNDS=4)
// Configuration des listeners pour les seekBars
seekBar1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Ajustement de la valeur au multiple de 10 le plus proche
int adjustedProgress = Math.round(progress / 10) * 10;
seekBar.setProgress(adjustedProgress);
questionCountValue.setText(String.valueOf(adjustedProgress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
seekBar2.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Mise à jour du gorgeesValue en fonction de la valeur de la seekBar2
gorgeesValue.setText(String.valueOf(progress));
// Mise à jour du textView5 en fonction de la valeur de la seekBar2
switch (progress) {
case 0:
textView5.setText("Palier : Grosse merde");
break;
case 2:
textView5.setText("Palier : Petite merde");
break;
case 4:
textView5.setText("Palier : Petit joueur");
break;
case 6:
textView5.setText("Palier : Un p'tit verre ?!");
break;
case 8:
textView5.setText("Palier : ça commence à aller");
break;
case 10:
textView5.setText("Palier : Alcoolique");
break;
case 12:
textView5.setText("Palier : COMA ETHYLIX");
break;
case 13:
textView5.setText("Palier : APÉROOOOO !!");
break;
case 14:
textView5.setText("Palier : LA J'SUIS BIENG");
break;
case 15:
textView5.setText("Palier : J'VOIS PLUS RIENG");
break;
case 17:
textView5.setText("Palier : J'AI PLUS DE VERRES");
break;
case 18 :
textView5.setText("Palier : Soirée Murge");
break;
case 19:
textView5.setText("Palier : Soirée Pétée");
break;
case 20:
textView5.setText("Palier : L'ENDER DRAGON");
break;
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// Configuration du checkBox // Q : IL sert à quoi ?
// R : Il sert à activer/désactiver les vues en dessous
buttonTestApi = findViewById(R.id.ButtonTestApi);
checkBoxGPT = findViewById(R.id.checkBoxGPT);
checkBoxGPT.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateOpenAICardState(isChecked);
}
});
// Initialiser l'état de la carte IA selon l'état initial du checkBox
updateOpenAICardState(checkBoxGPT.isChecked());
// Configuration de la seekBar3
seekBar3.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar3, int progress, boolean fromUser) {
textViewRatioGen.setText("Ratio BDD/OPENAI : 1/" + progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar3) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar3) {
}
});
// Configuration de la seekBarDuration
seekBarDuration.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Afficher avec signe +/- pour bien voir l'offset, mais sans signe pour 0
String displayValue;
if (progress > 0) {
displayValue = "+" + progress;
} else {
displayValue = String.valueOf(progress); // Affiche "0" ou "-X"
}
durationValue.setText(displayValue);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// Partie OpenAI : enregistrement de la clé en dur.
// Récupérer une instance des SharedPreferences
SharedPreferences sharedPreferences = getSharedPreferences("MyPrefs", MODE_PRIVATE);
final SharedPreferences.Editor editor = sharedPreferences.edit();
// Récupérer la valeur enregistrée dans les SharedPreferences
String savedText = sharedPreferences.getString("savedText", "");
editTextKeyGPT.setText(savedText);
// Enregistrer le contenu de l'EditText lorsque l'utilisateur modifie le texte
editTextKeyGPT.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
// Enregistrer le texte dans les SharedPreferences
editor.putString("savedText", editTextKeyGPT.getText().toString());
editor.apply();
}
});
}
public void onClickButtonTestAPI(View view) {
String apiKey = editTextKeyGPT.getText().toString();
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) {
String operation = "Test de connexion API " + selectedProvider.getDisplayName();
String details = "Échec de connexion lors du test de l'API";
ErrorHandler.logErrorOnly("BoideloClassicParamsActivity", 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 {
if (response.isSuccessful()) {
runOnUiThread(() -> {
Toast.makeText(getApplicationContext(),
"Connexion " + selectedProvider.getDisplayName() + " réussie !",
Toast.LENGTH_SHORT).show();
});
} else {
runOnUiThread(() -> {
Toast.makeText(getApplicationContext(),
"Erreur " + selectedProvider.getDisplayName() + " (HTTP " + response.code() + ")",
Toast.LENGTH_SHORT).show();
});
}
response.close();
}
});
}
/**
* Initialise les vues de l'activité
* Met à jour l'état de la carte OpenAI (visibilité et taille)
* Quand l'IA est désactivée, cache tous les éléments sauf le titre et le switch
*/
private void initViews() {
// TODO: Initialiser les vues pour les paramètres
private void updateOpenAICardState(boolean isOpenAIEnabled) {
// Activation/désactivation des vues en fonction de l'état du checkBox
autoCompleteProvider.setEnabled(isOpenAIEnabled);
// Pour le champ API key : on garde le layout activé pour le toggle password,
// mais on désactive l'édition du texte
editTextKeyGPT.setFocusable(isOpenAIEnabled);
editTextKeyGPT.setFocusableInTouchMode(isOpenAIEnabled);
editTextKeyGPT.setClickable(isOpenAIEnabled);
editTextKeyGPT.setCursorVisible(isOpenAIEnabled);
if (!isOpenAIEnabled) {
editTextKeyGPT.clearFocus();
}
textViewRatioGen.setEnabled(isOpenAIEnabled);
seekBar3.setEnabled(isOpenAIEnabled);
buttonTestApi.setEnabled(isOpenAIEnabled);
// Cacher/montrer les éléments pour réduire la taille de la carte
// Les éléments à cacher quand l'IA est désactivée (index 2 à 6 dans le LinearLayout)
// Index: 0=titreLinearLayout, 1=textInputLayoutProvider, 2=textInputLayoutApiKey,
// 3=ratioSeekBarLinearLayout, 4=buttonTestApi
if (openaiContentLayout != null) {
for (int i = 1; i < openaiContentLayout.getChildCount(); i++) {
View child = openaiContentLayout.getChildAt(i);
if (child != null) {
child.setVisibility(isOpenAIEnabled ? View.VISIBLE : View.GONE);
}
}
}
}
/**
* Configure les écouteurs d'événements
*/
private void setupListeners() {
// TODO: Configurer les listeners pour les changements de paramètres
}
public void onClickButtonStart(View view) {
// Récupérer les paramètres de la partie
int nombreQuestions = seekBar1.getProgress();
int ajoutGorgees = seekBar2.getProgress();
int ratioBddOpenAI = seekBar3.getProgress();
int durationDefis = seekBarDuration.getProgress();
boolean openAI = checkBoxGPT.isChecked();
/**
* Charge les paramètres actuels depuis les préférences
*/
private void loadCurrentSettings() {
// TODO: Charger les paramètres depuis SharedPreferences
}
// Récupérer les joueurs depuis l'intent
toutlesjoueurs = getIntent().getStringArrayListExtra("EXTRA_LIST_JOUEUR");
/**
* Sauvegarde les paramètres
*/
private void saveSettings() {
// TODO: Sauvegarder les paramètres dans SharedPreferences
if (toutlesjoueurs == null || toutlesjoueurs.isEmpty()) {
Toast.makeText(this, "Erreur: Aucun joueur trouvé", Toast.LENGTH_SHORT).show();
return;
}
// Lancer l'activité BoideloClassicGameActivity avec les paramètres
Intent intent = new Intent(this, BoideloClassicGameActivity.class);
intent.putExtra("EXTRA_NOMBRE_QUESTIONS", nombreQuestions);
intent.putExtra("EXTRA_AJOUT_GORGEE", ajoutGorgees);
intent.putExtra("EXTRA_RATIO_OPENAI", ratioBddOpenAI);
intent.putExtra("EXTRA_DURATION_DEFIS", durationDefis);
intent.putExtra("EXTRA_OPENAI", openAI);
intent.putExtra("EXTRA_KEY_OPENAI", editTextKeyGPT.getText().toString());
intent.putExtra("EXTRA_AI_PROVIDER", selectedProvider.name());
intent.putStringArrayListExtra("PLAYERS", toutlesjoueurs);
startActivity(intent);
}
@Override
@@ -33,7 +33,7 @@ public class BoideloClassicSetupActivity extends AppCompatActivity {
private MaterialButton startGameButton;
private TextView playerCountText;
private MaterialToolbar toolbar;
private final List<String> playerNames = new ArrayList<>();
@Override
@@ -74,8 +74,8 @@ public class BoideloClassicSetupActivity extends AppCompatActivity {
Toast.makeText(this, "Maximum " + MAX_PLAYERS + " joueurs", Toast.LENGTH_SHORT).show();
}
});
startGameButton.setOnClickListener(v -> startGame());
startGameButton.setOnClickListener(v -> goToParams());
}
private void addPlayerRow() {
@@ -155,17 +155,20 @@ public class BoideloClassicSetupActivity extends AppCompatActivity {
int validPlayers = playersContainer.getChildCount();
boolean canStart = validPlayers >= MIN_PLAYERS;
startGameButton.setEnabled(canStart);
startGameButton.setText(canStart ? "JOUER (" + validPlayers + ")" : "Ajoutez des joueurs");
startGameButton.setText(canStart ? "PARAMÈTRES ET JEU (" + validPlayers + ")" : "Ajoutez des joueurs");
}
private void startGame() {
/**
* Redirige vers l'écran des paramètres avant de lancer le jeu
*/
private void goToParams() {
// Vérifier que tous les champs minimums sont remplis
ArrayList<String> validNames = new ArrayList<>();
for (int i = 0; i < playersContainer.getChildCount(); i++) {
View row = playersContainer.getChildAt(i);
TextInputEditText edit = row.findViewById(R.id.playerName);
String name = edit.getText().toString().trim();
if (TextUtils.isEmpty(name)) {
Toast.makeText(this, "Veuillez remplir le nom du joueur " + (i + 1), Toast.LENGTH_SHORT).show();
edit.requestFocus();
@@ -173,14 +176,15 @@ public class BoideloClassicSetupActivity extends AppCompatActivity {
}
validNames.add(name);
}
if (validNames.size() < MIN_PLAYERS) {
Toast.makeText(this, "Minimum " + MIN_PLAYERS + " joueurs requis", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(this, BoideloClassicGameActivity.class);
intent.putStringArrayListExtra("PLAYERS", validNames);
// Rediriger vers l'écran des paramètres
Intent intent = new Intent(this, BoideloClassicParamsActivity.class);
intent.putStringArrayListExtra("EXTRA_LIST_JOUEUR", validNames);
startActivity(intent);
}
}
@@ -0,0 +1,348 @@
package com.example.boidelov3.games.papelito;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
/**
* Logique du jeu Papelito (Undercover)
*/
public class PapelitoGame {
private final List<PapelitoPlayer> players;
private final List<PapelitoPlayer> alivePlayers;
private final List<String> wordPairs;
private String currentCivilWord;
private String currentUndercoverWord;
private int currentPlayerIndex;
private final Random random;
private GameState gameState;
public enum GameState {
SETUP,
DISCUSSION,
VOTING,
RESULT,
GAME_OVER
}
// Paires de mots pour le jeu (civil / undercover)
private static final String[][] DEFAULT_WORD_PAIRS = {
{"Pizza", "Burger"},
{"Facebook", "Instagram"},
{"Chat", "Chien"},
{"Foot", "Basket"},
{"Vin", "Bière"},
{"Mer", "Montagne"},
{"Avion", "Hélicoptère"},
{"Piano", "Guitare"},
{"Fromage", "Dessert"},
{"École", "Fac"},
{"Mariage", "Divorce"},
{"Hôpital", "Cabinet"},
{"Boulangerie", "Boucherie"},
{"Zombie", "Vampire"},
{"Pirate", "Ninja"},
{"Cowboy", "Indien"},
{"Fée", "Sorcière"},
{"Robot", "Alien"},
{"Dragon", "Licorne"}
};
public PapelitoGame() {
this.players = new ArrayList<>();
this.alivePlayers = new ArrayList<>();
this.wordPairs = new ArrayList<>();
this.random = new Random();
this.gameState = GameState.SETUP;
// Ajouter les paires de mots par défaut
for (String[] pair : DEFAULT_WORD_PAIRS) {
wordPairs.add(pair[0] + "|" + pair[1]);
}
}
/**
* Configure une nouvelle partie
*/
public void setupGame(List<String> playerNames, int undercoverCount) {
players.clear();
// Créer les joueurs
for (String name : playerNames) {
players.add(new PapelitoPlayer(name));
}
// Vérifier qu'on a assez de joueurs
if (undercoverCount >= players.size()) {
throw new IllegalArgumentException("Il faut plus de joueurs que d'undercovers");
}
// Choisir une paire de mots aléatoire
String[] words = selectRandomWordPair();
currentCivilWord = words[0];
currentUndercoverWord = words[1];
// Assigner les rôles
assignRoles(undercoverCount);
// Initialiser la liste des joueurs vivants
alivePlayers.clear();
alivePlayers.addAll(players);
gameState = GameState.DISCUSSION;
}
/**
* Sélectionne une paire de mots aléatoire
*/
private String[] selectRandomWordPair() {
String pair = wordPairs.get(random.nextInt(wordPairs.size()));
String[] words = pair.split("\\|");
if (words.length != 2) {
throw new IllegalStateException("Invalid word pair format: " + pair);
}
return words;
}
/**
* Assigne les rôles aux joueurs
*/
private void assignRoles(int undercoverCount) {
if (players.isEmpty()) return;
// Mélanger les joueurs
List<PapelitoPlayer> shuffled = new ArrayList<>(players);
Collections.shuffle(shuffled);
// Assigner les undercovers
for (int i = 0; i < undercoverCount; i++) {
shuffled.get(i).setRole(PapelitoPlayer.Role.UNDERCOVER);
shuffled.get(i).setSecretWord(currentUndercoverWord);
}
// Le reste sont des civils
for (int i = undercoverCount; i < shuffled.size(); i++) {
shuffled.get(i).setRole(PapelitoPlayer.Role.CIVIL);
shuffled.get(i).setSecretWord(currentCivilWord);
}
}
/**
* Enregistre un vote contre un joueur
*/
public boolean vote(PapelitoPlayer voter, PapelitoPlayer votedPlayer) {
if (!voter.isAlive() || !votedPlayer.isAlive()) {
return false;
}
if (!gameState.equals(GameState.VOTING)) {
return false;
}
// Vérifier que le joueur n'a pas déjà voté
// (pour simplifier, on ne track pas qui a voté, chaque joueur peut voter une fois)
votedPlayer.addVote();
return true;
}
/**
* Élimine le joueur avec le plus de votes
* Retourne null si pas de votes ou en cas d'égalité
*/
public PapelitoPlayer eliminateMostVoted() {
if (alivePlayers.isEmpty()) {
return null;
}
PapelitoPlayer mostVoted = null;
int maxVotes = -1;
int playersWithMaxVotes = 0;
// Trouver le nombre maximum de votes
for (PapelitoPlayer player : alivePlayers) {
if (player.getVotesReceived() > maxVotes) {
maxVotes = player.getVotesReceived();
mostVoted = player;
playersWithMaxVotes = 1;
} else if (player.getVotesReceived() == maxVotes && maxVotes > 0) {
// Égalité détectée
playersWithMaxVotes++;
}
}
// Si pas de votes ou égalité entre plusieurs joueurs, personne n'est éliminé
if (maxVotes <= 0 || playersWithMaxVotes > 1) {
return null;
}
if (mostVoted != null) {
mostVoted.eliminate();
alivePlayers.remove(mostVoted);
// Révéler son rôle
return mostVoted;
}
return null;
}
/**
* Vérifie si la partie est terminée
*/
public boolean checkGameOver() {
int civilsCount = 0;
int undercoversCount = 0;
for (PapelitoPlayer player : alivePlayers) {
if (player.isCivil()) {
civilsCount++;
} else if (player.isUndercover()) {
undercoversCount++;
}
}
// Les civils gagnent s'il n'y a plus d'undercovers
if (undercoversCount == 0) {
gameState = GameState.GAME_OVER;
return true;
}
// Les undercovers gagnent s'ils sont en nombre égal ou supérieur aux civils
// Explication: Si les undercovers sont égal ou plus nombreux, les civils ne peuvent plus gagner
// car il n'y aurait plus assez de civils pour voter et éliminer tous les undercovers
if (undercoversCount >= civilsCount) {
gameState = GameState.GAME_OVER;
return true;
}
return false;
}
/**
* Obtient le joueur actuel
*/
public PapelitoPlayer getCurrentPlayer() {
if (!alivePlayers.isEmpty()) {
// Utilise alivePlayers pour les joueurs vivants
if (currentPlayerIndex >= 0 && currentPlayerIndex < alivePlayers.size()) {
return alivePlayers.get(currentPlayerIndex);
}
}
return null;
}
/**
* Passe au joueur suivant
*/
public void nextPlayer() {
if (!alivePlayers.isEmpty()) {
currentPlayerIndex = (currentPlayerIndex + 1) % alivePlayers.size();
}
}
/**
* Obtient le nombre de civils vivants
*/
public int getAliveCivilsCount() {
int count = 0;
for (PapelitoPlayer player : alivePlayers) {
if (player.isCivil()) {
count++;
}
}
return count;
}
/**
* Obtient le nombre d'undercovers vivants
*/
public int getAliveUndercoversCount() {
int count = 0;
for (PapelitoPlayer player : alivePlayers) {
if (player.isUndercover()) {
count++;
}
}
return count;
}
/**
* Obtient les gagnants
*/
public PapelitoPlayer.Role getWinningTeam() {
int civilsCount = getAliveCivilsCount();
int undercoversCount = getAliveUndercoversCount();
if (undercoversCount == 0) {
return PapelitoPlayer.Role.CIVIL;
} else if (undercoversCount >= civilsCount) {
return PapelitoPlayer.Role.UNDERCOVER;
}
return null;
}
/**
* Réinitialise les votes pour un nouveau tour
*/
public void resetVotes() {
for (PapelitoPlayer player : players) {
player.resetVotes();
}
}
// Getters
public List<PapelitoPlayer> getPlayers() {
return new ArrayList<>(players);
}
public List<PapelitoPlayer> getAlivePlayers() {
return new ArrayList<>(alivePlayers);
}
public String getCurrentCivilWord() {
return currentCivilWord;
}
public String getCurrentUndercoverWord() {
return currentUndercoverWord;
}
public GameState getGameState() {
return gameState;
}
public void setGameState(GameState state) {
// Validation des transitions légales entre états
GameState current = this.gameState;
// Transitions légales autorisées
if (current == GameState.SETUP && state != GameState.DISCUSSION && state != GameState.SETUP) {
throw new IllegalStateException("Cannot transition from SETUP to " + state);
}
if (current == GameState.DISCUSSION && state != GameState.VOTING && state != GameState.SETUP) {
throw new IllegalStateException("Cannot transition from DISCUSSION to " + state);
}
if (current == GameState.VOTING && state != GameState.RESULT && state != GameState.SETUP) {
throw new IllegalStateException("Cannot transition from VOTING to " + state);
}
if (current == GameState.RESULT && state != GameState.DISCUSSION && state != GameState.GAME_OVER && state != GameState.SETUP) {
throw new IllegalStateException("Cannot transition from RESULT to " + state);
}
if (current == GameState.GAME_OVER && state != GameState.SETUP) {
throw new IllegalStateException("Cannot transition from GAME_OVER to " + state);
}
this.gameState = state;
}
public void reset() {
players.clear();
alivePlayers.clear();
currentPlayerIndex = 0;
gameState = GameState.SETUP;
}
}
@@ -0,0 +1,558 @@
package com.example.boidelov3.games.papelito;
import android.content.Intent;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.example.boidelov3.R;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Activité principale du jeu Papelito (Undercover)
*
* Gère le déroulement complet du jeu :
* - Affichage du mot secret à chaque joueur
* - Phase de discussion avec timer
* - Phase de vote
* - Affichage des résultats
* - Condition de victoire
*/
public class PapelitoGameActivity extends AppCompatActivity {
// UI Components
private MaterialToolbar toolbar;
private TextView phaseTextView;
private TextView infoTextView;
private TextView timerTextView;
private MaterialCardView wordCard;
private TextView wordTextView;
private TextView currentWordPlayerTextView;
private MaterialButton showWordButton;
private MaterialButton startDiscussionButton;
private MaterialButton startVotingButton;
private MaterialButton nextPlayerButton;
private MaterialButton endGameButton;
private View mainContent;
// Game Logic
private PapelitoGame game;
private List<String> playerNames;
private int undercoverCount;
private int discussionTimeSeconds;
private CountDownTimer discussionTimer;
private int currentPlayerViewIndex;
private boolean hasShownWordToPlayer;
private List<String> playersWhoVoted;
// Constants
private static final int DEFAULT_DISCUSSION_TIME = 60; // 60 secondes
private static final int DEFAULT_UNDERCOVER_COUNT = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_papelito_game);
// Get intent data with validation
playerNames = getIntent().getStringArrayListExtra("PLAYERS");
undercoverCount = getIntent().getIntExtra("UNDERCOVER_COUNT", DEFAULT_UNDERCOVER_COUNT);
discussionTimeSeconds = getIntent().getIntExtra("DISCUSSION_TIME", DEFAULT_DISCUSSION_TIME);
// Validate all intent extras
if (playerNames == null || playerNames.isEmpty()) {
Toast.makeText(this, "Erreur: Aucun joueur spécifié pour la partie", Toast.LENGTH_LONG).show();
finish();
return;
}
if (undercoverCount <= 0) {
Toast.makeText(this, "Erreur: Le nombre d'undercovers doit être au moins de 1", Toast.LENGTH_LONG).show();
finish();
return;
}
if (undercoverCount >= playerNames.size()) {
Toast.makeText(this, "Erreur: Il doit y avoir plus de joueurs que d'undercovers", Toast.LENGTH_LONG).show();
finish();
return;
}
if (discussionTimeSeconds <= 0) {
Toast.makeText(this, "Erreur: La durée de discussion doit être positive", Toast.LENGTH_LONG).show();
finish();
return;
}
initViews();
setupToolbar();
setupGame();
setupListeners();
// Commencer par afficher le mot au premier joueur
startWordRevealPhase();
}
private void initViews() {
toolbar = findViewById(R.id.toolbar);
phaseTextView = findViewById(R.id.phaseTextView);
infoTextView = findViewById(R.id.infoTextView);
timerTextView = findViewById(R.id.timerTextView);
wordCard = findViewById(R.id.wordCard);
wordTextView = findViewById(R.id.wordTextView);
currentWordPlayerTextView = findViewById(R.id.currentWordPlayerTextView);
showWordButton = findViewById(R.id.showWordButton);
startDiscussionButton = findViewById(R.id.startDiscussionButton);
startVotingButton = findViewById(R.id.startVotingButton);
nextPlayerButton = findViewById(R.id.nextPlayerButton);
endGameButton = findViewById(R.id.endGameButton);
mainContent = findViewById(R.id.mainContent);
}
private void setupToolbar() {
toolbar.setNavigationOnClickListener(v -> showExitConfirmationDialog());
}
private void setupGame() {
game = new PapelitoGame();
game.setupGame(playerNames, undercoverCount);
currentPlayerViewIndex = 0;
hasShownWordToPlayer = false;
playersWhoVoted = new ArrayList<>();
}
private void setupListeners() {
showWordButton.setOnClickListener(v -> showSecretWord());
nextPlayerButton.setOnClickListener(v -> moveToNextPlayer());
startDiscussionButton.setOnClickListener(v -> startDiscussionPhase());
startVotingButton.setOnClickListener(v -> startVotingPhase());
endGameButton.setOnClickListener(v -> showGameOverDialog());
}
// ============================================================
// PHASE 1: RÉVÉLATION DES MOTS
// ============================================================
private void startWordRevealPhase() {
game.setGameState(PapelitoGame.GameState.SETUP);
// Reset game's currentPlayerIndex to prevent wrong player start
currentPlayerViewIndex = 0;
hasShownWordToPlayer = false;
updateUIForWordReveal();
showCurrentPlayerPrompt();
}
private void updateUIForWordReveal() {
phaseTextView.setText("Phase 1: Révélation des mots");
timerTextView.setVisibility(View.GONE);
// Cacher la carte du mot
wordCard.setVisibility(View.GONE);
wordTextView.setText("");
// Afficher le bouton pour montrer le mot
showWordButton.setVisibility(View.VISIBLE);
nextPlayerButton.setVisibility(View.VISIBLE);
// Cacher les boutons de phase suivante
startDiscussionButton.setVisibility(View.GONE);
startVotingButton.setVisibility(View.GONE);
endGameButton.setVisibility(View.GONE);
}
private void showCurrentPlayerPrompt() {
PapelitoPlayer player = game.getPlayers().get(currentPlayerViewIndex);
currentWordPlayerTextView.setText(
String.format("Tour de %s", player.getName())
);
infoTextView.setText(
"Passe le téléphone à " + player.getName() + "\n\n" +
"Appuie sur 'Voir mon mot' pour découvrir ton mot secret."
);
hasShownWordToPlayer = false;
showWordButton.setEnabled(true);
}
private void showSecretWord() {
PapelitoPlayer player = game.getPlayers().get(currentPlayerViewIndex);
String secretWord = player.getSecretWord();
wordTextView.setText(secretWord);
wordCard.setVisibility(View.VISIBLE);
showWordButton.setEnabled(false);
hasShownWordToPlayer = true;
infoTextView.setText(
"Ton mot est: " + secretWord + "\n\n" +
"Mémorise-le bien et appuie sur 'Joueur suivant' " +
"quand tu es prêt."
);
}
private void moveToNextPlayer() {
if (!hasShownWordToPlayer) {
Toast.makeText(this,
"Tu dois d'abord voir ton mot!",
Toast.LENGTH_SHORT).show();
return;
}
currentPlayerViewIndex++;
if (currentPlayerViewIndex >= game.getPlayers().size()) {
// Tous les joueurs ont vu leur mot
showReadyForDiscussionDialog();
} else {
showCurrentPlayerPrompt();
wordCard.setVisibility(View.GONE);
}
}
private void showReadyForDiscussionDialog() {
currentWordPlayerTextView.setText("Prêts!");
infoTextView.setText(
"Tous les joueurs ont vu leur mot.\n\n" +
"Préparez-vous pour la phase de discussion!"
);
wordCard.setVisibility(View.GONE);
showWordButton.setVisibility(View.GONE);
nextPlayerButton.setVisibility(View.GONE);
startDiscussionButton.setVisibility(View.VISIBLE);
}
// ============================================================
// PHASE 2: DISCUSSION
// ============================================================
private void startDiscussionPhase() {
game.setGameState(PapelitoGame.GameState.DISCUSSION);
updateUIForDiscussion();
startDiscussionTimer();
}
private void updateUIForDiscussion() {
phaseTextView.setText("Phase 2: Discussion");
currentWordPlayerTextView.setText("Discutez!");
wordCard.setVisibility(View.GONE);
showWordButton.setVisibility(View.GONE);
nextPlayerButton.setVisibility(View.GONE);
startDiscussionButton.setVisibility(View.GONE);
timerTextView.setVisibility(View.VISIBLE);
startVotingButton.setVisibility(View.VISIBLE);
infoTextView.setText(
"Chaque joueur décrit son mot tour à tour.\n\n" +
"Les Undercovers doivent essayer de deviner le mot des civils " +
"sans se faire repérer.\n\n" +
"Les civils doivent essayer d'identifier les Undercovers."
);
}
private void startDiscussionTimer() {
discussionTimer = new CountDownTimer(
discussionTimeSeconds * 1000L, 1000
) {
@Override
public void onTick(long millisUntilFinished) {
long minutes = millisUntilFinished / 60000;
long seconds = (millisUntilFinished % 60000) / 1000;
timerTextView.setText(
String.format(Locale.getDefault(),
"%d:%02d", minutes, seconds)
);
}
@Override
public void onFinish() {
timerTextView.setText("0:00");
Toast.makeText(PapelitoGameActivity.this,
"Temps écoulé! Passez au vote.",
Toast.LENGTH_LONG).show();
}
}.start();
}
// ============================================================
// PHASE 3: VOTE
// ============================================================
private void startVotingPhase() {
// Arrêter le timer de discussion
if (discussionTimer != null) {
discussionTimer.cancel();
}
game.setGameState(PapelitoGame.GameState.VOTING);
game.resetVotes();
playersWhoVoted.clear();
updateUIForVoting();
showVotingDialog();
}
private void updateUIForVoting() {
phaseTextView.setText("Phase 3: Vote");
timerTextView.setVisibility(View.GONE);
startVotingButton.setVisibility(View.GONE);
endGameButton.setVisibility(View.VISIBLE);
infoTextView.setText(
"Chaque joueur vivant vote pour éliminer quelqu'un.\n\n" +
"Le joueur avec le plus de votes est éliminé."
);
}
private void showVotingDialog() {
List<PapelitoPlayer> alivePlayers = game.getAlivePlayers();
if (alivePlayers.isEmpty()) {
checkGameEnd();
return;
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Vote pour éliminer");
// Créer le layout personnalisé
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 40, 50, 10);
TextView label = new TextView(this);
label.setText("Qui voulez-vous éliminer?");
label.setPadding(0, 0, 0, 20);
layout.addView(label);
// Créer une grille de boutons pour chaque joueur vivant
GridLayout playerGrid = new GridLayout(this);
playerGrid.setColumnCount(2);
for (PapelitoPlayer player : alivePlayers) {
MaterialButton playerButton = new MaterialButton(this);
playerButton.setText(player.getName());
playerButton.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.setMargins(8, 8, 8, 8);
playerButton.setLayoutParams(params);
playerButton.setOnClickListener(v -> {
recordVote(player);
});
playerGrid.addView(playerButton);
}
layout.addView(playerGrid);
builder.setView(layout);
builder.setCancelable(false);
builder.show();
}
private void recordVote(PapelitoPlayer votedPlayer) {
if (playersWhoVoted.size() >= game.getAlivePlayers().size()) {
// Tous ont voté
return;
}
// Enregistrer le vote (simplifié : on ne track pas QUI a voté)
votedPlayer.addVote();
playersWhoVoted.add("vote");
Toast.makeText(this,
"Vote enregistré pour " + votedPlayer.getName(),
Toast.LENGTH_SHORT).show();
// Vérifier si tous les joueurs vivants ont voté
if (playersWhoVoted.size() >= game.getAlivePlayers().size()) {
// Tous les votes sont en, afficher le résultat
showVotingResult();
} else {
// Continuer avec le prochain voteur
showNextVoterDialog();
}
}
private void showNextVoterDialog() {
int votesRemaining = game.getAlivePlayers().size() - playersWhoVoted.size();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Vote en cours");
builder.setMessage(
"Joueurs ayant voté: " + playersWhoVoted.size() + " / " +
game.getAlivePlayers().size() + "\n\n" +
"Passe le téléphone au prochain joueur."
);
builder.setPositiveButton("Continuer", (dialog, which) -> {
showVotingDialog();
});
builder.setCancelable(false);
builder.show();
}
private void showVotingResult() {
PapelitoPlayer eliminated = game.eliminateMostVoted();
if (eliminated == null) {
// Personne n'a été éliminé (pas de votes ou égalité)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Vote nul!");
builder.setMessage(
"Aucun joueur n'a été éliminé.\n\n" +
"Soit il n'y avait pas de votes, soit il y a une égalité.\n\n" +
"La partie continue!"
);
builder.setPositiveButton("Continuer", (dialog, which) -> {
checkGameEnd();
});
builder.setCancelable(false);
builder.show();
return;
}
// Afficher le résultat de l'élimination
showEliminationResult(eliminated);
}
private void showEliminationResult(PapelitoPlayer eliminated) {
game.setGameState(PapelitoGame.GameState.RESULT);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Joueur éliminé!");
String message = String.format(
"%s a été éliminé!\n\n" +
"Son rôle était: %s\n\n" +
"Son mot était: %s",
eliminated.getName(),
eliminated.getRole().getDisplayName(),
eliminated.getSecretWord()
);
builder.setMessage(message);
builder.setPositiveButton("Continuer", (dialog, which) -> {
checkGameEnd();
});
builder.setCancelable(false);
builder.show();
}
// ============================================================
// VÉRIFICATION DE FIN DE JEU
// ============================================================
private void checkGameEnd() {
boolean gameOver = game.checkGameOver();
if (gameOver) {
showGameOverDialog();
} else {
// Continuer avec un nouveau tour
showNextRoundDialog();
}
}
private void showNextRoundDialog() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Nouveau tour");
StringBuilder status = new StringBuilder();
status.append("Joueurs vivants:\n\n");
for (PapelitoPlayer player : game.getAlivePlayers()) {
status.append("• ").append(player.getName()).append("\n");
}
status.append("\nCivils: ").append(game.getAliveCivilsCount());
status.append("\nUndercovers: ").append(game.getAliveUndercoversCount());
builder.setMessage(status.toString());
builder.setPositiveButton("Commencer", (dialog, which) -> {
startWordRevealPhase();
});
builder.setCancelable(false);
builder.show();
}
private void showGameOverDialog() {
game.setGameState(PapelitoGame.GameState.GAME_OVER);
// Lancer l'activité de résultat
Intent intent = new Intent(this, PapelitoResultActivity.class);
intent.putExtra(PapelitoResultActivity.EXTRA_PLAYERS, new ArrayList<>(game.getPlayers()));
intent.putExtra(PapelitoResultActivity.EXTRA_WINNING_TEAM, game.getWinningTeam());
intent.putExtra(PapelitoResultActivity.EXTRA_CIVIL_WORD, game.getCurrentCivilWord());
intent.putExtra(PapelitoResultActivity.EXTRA_UNDERCOVER_WORD, game.getCurrentUndercoverWord());
intent.putExtra(PapelitoResultActivity.EXTRA_TOTAL_ROUNDS, playersWhoVoted.size());
startActivity(intent);
finish();
}
// ============================================================
// DIVERS
// ============================================================
private void showExitConfirmationDialog() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("Quitter la partie?");
builder.setMessage(
"La partie sera perdue si vous quittez.\n\n" +
"Voulez-vous vraiment quitter?"
);
builder.setPositiveButton("Quitter", (dialog, which) -> {
finish();
});
builder.setNegativeButton("Continuer", null);
builder.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (discussionTimer != null) {
discussionTimer.cancel();
discussionTimer = null;
}
}
@Override
protected void onPause() {
super.onPause();
if (discussionTimer != null) {
discussionTimer.cancel();
}
}
@Override
public void onBackPressed() {
showExitConfirmationDialog();
}
}
@@ -0,0 +1,109 @@
package com.example.boidelov3.games.papelito;
/**
* Représente un joueur du jeu Papelito (Undercover)
*/
public class PapelitoPlayer implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private String secretWord;
private Role role;
private boolean isAlive;
private int votesReceived;
public enum Role {
CIVIL("Civil"),
UNDERCOVER("Undercover"),
MR_WHITE("Mr. White");
private final String displayName;
Role(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
public PapelitoPlayer(String name) {
this.name = name;
this.isAlive = true;
this.votesReceived = 0;
this.role = null; // Sera assigné au début du jeu
}
public PapelitoPlayer(String name, Role role, String secretWord) {
this.name = name;
this.role = role;
this.secretWord = secretWord;
this.isAlive = true;
this.votesReceived = 0;
}
public String getName() {
return name;
}
public String getSecretWord() {
return secretWord;
}
public void setSecretWord(String secretWord) {
this.secretWord = secretWord;
}
public Role getRole() {
return role;
}
/**
* Retourne le rôle du joueur.
* @return le rôle du joueur, peut être null si non assigné
*/
public void setRole(Role role) {
this.role = role;
}
public boolean isAlive() {
return isAlive;
}
public void setAlive(boolean alive) {
isAlive = alive;
}
public void eliminate() {
this.isAlive = false;
}
public int getVotesReceived() {
return votesReceived;
}
public void addVote() {
this.votesReceived++;
}
public void resetVotes() {
this.votesReceived = 0;
}
public boolean isMrWhite() {
return role != null && role == Role.MR_WHITE;
}
public boolean isUndercover() {
return role != null && role == Role.UNDERCOVER;
}
public boolean isCivil() {
return role != null && role == Role.CIVIL;
}
@Override
public String toString() {
return name + " (" + (role != null ? role.getDisplayName() : "?") + ")";
}
}
@@ -0,0 +1,134 @@
package com.example.boidelov3.games.papelito;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.boidelov3.R;
import com.example.boidelov3.hub.GameSelectionActivity;
import java.util.ArrayList;
import java.util.Locale;
/**
* Activité de fin de partie Papelito
* Affiche les résultats, révèle tous les rôles et permet de rejouer
*/
public class PapelitoResultActivity extends AppCompatActivity {
public static final String EXTRA_PLAYERS = "extra_players";
public static final String EXTRA_WINNING_TEAM = "extra_winning_team";
public static final String EXTRA_CIVIL_WORD = "extra_civil_word";
public static final String EXTRA_UNDERCOVER_WORD = "extra_undercover_word";
public static final String EXTRA_TOTAL_ROUNDS = "extra_total_rounds";
private ArrayList<PapelitoPlayer> players;
private PapelitoPlayer.Role winningTeam;
private String civilWord;
private String undercoverWord;
private int totalRounds;
private TextView textViewWinner;
private TextView textViewWords;
private TextView textViewRounds;
private RecyclerView recyclerViewResults;
private Button buttonNewGame;
private Button buttonHome;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_papelito_result);
// Récupérer les données
players = (ArrayList<PapelitoPlayer>) getIntent().getSerializableExtra(EXTRA_PLAYERS);
winningTeam = (PapelitoPlayer.Role) getIntent().getSerializableExtra(EXTRA_WINNING_TEAM);
civilWord = getIntent().getStringExtra(EXTRA_CIVIL_WORD);
undercoverWord = getIntent().getStringExtra(EXTRA_UNDERCOVER_WORD);
totalRounds = getIntent().getIntExtra(EXTRA_TOTAL_ROUNDS, 0);
// Add validation
if (players == null || players.isEmpty()) {
finish();
return;
}
// Initialiser les vues
initViews();
// Afficher les résultats
displayResults();
}
private void initViews() {
textViewWinner = findViewById(R.id.winnerTextView);
textViewWords = findViewById(R.id.undercoverWordTextView);
textViewRounds = findViewById(R.id.roundsTextView);
recyclerViewResults = findViewById(R.id.rolesRecyclerView);
buttonNewGame = findViewById(R.id.newGameButton);
buttonHome = findViewById(R.id.homeButton);
// Configurer le RecyclerView
recyclerViewResults.setLayoutManager(new LinearLayoutManager(this));
// Bouton Nouvelle Partie
buttonNewGame.setOnClickListener(v -> {
Intent intent = new Intent(this, PapelitoSetupActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
});
// Bouton Retour Hub
buttonHome.setOnClickListener(v -> {
Intent intent = new Intent(this, GameSelectionActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
});
}
private void displayResults() {
// Afficher l'équipe gagnante
if (winningTeam != null) {
String winnerText;
int backgroundColor;
if (winningTeam == PapelitoPlayer.Role.CIVIL) {
winnerText = "🎉 LES CIVILS ONT GAGNÉ !";
backgroundColor = getColor(R.color.civil_bg);
} else {
winnerText = "🔴 LES UNDERCOVERS ONT GAGNÉ !";
backgroundColor = getColor(R.color.undercover_bg);
}
textViewWinner.setText(winnerText);
findViewById(R.id.winnerCard).setBackgroundColor(backgroundColor);
}
// Afficher les mots (civilWordTextView et undercoverWordTextView)
TextView civilWordView = findViewById(R.id.civilWordTextView);
TextView undercoverWordView = findViewById(R.id.undercoverWordTextView);
if (civilWordView != null) {
civilWordView.setText("Mot Civil: " + (civilWord != null ? civilWord : "Non défini"));
}
if (undercoverWordView != null) {
undercoverWordView.setText("Mot Undercover: " + (undercoverWord != null ? undercoverWord : "Non défini"));
}
// Afficher le nombre de tours
String roundsText = "Nombre de manches: " + totalRounds;
textViewRounds.setText(roundsText);
// Configurer l'adaptateur pour afficher tous les joueurs
PapelitoResultAdapter adapter = new PapelitoResultAdapter(players, civilWord, undercoverWord);
recyclerViewResults.setAdapter(adapter);
}
}
@@ -0,0 +1,118 @@
package com.example.boidelov3.games.papelito;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.boidelov3.R;
import java.util.ArrayList;
/**
* Adaptateur pour afficher les résultats finaux de tous les joueurs
*/
public class PapelitoResultAdapter extends RecyclerView.Adapter<PapelitoResultAdapter.ResultViewHolder> {
private final ArrayList<PapelitoPlayer> players;
private final String civilWord;
private final String undercoverWord;
public PapelitoResultAdapter(ArrayList<PapelitoPlayer> players, String civilWord, String undercoverWord) {
this.players = players;
this.civilWord = civilWord;
this.undercoverWord = undercoverWord;
}
@NonNull
@Override
public ResultViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_papelito_result, parent, false);
return new ResultViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ResultViewHolder holder, int position) {
PapelitoPlayer player = players.get(position);
// Nom du joueur
holder.textViewPlayerName.setText(player.getName());
// Rôle avec vérification null
if (player.getRole() != null) {
holder.textViewRole.setText(player.getRole().getDisplayName());
} else {
holder.textViewRole.setText("?");
}
// Statut (éliminé ou survivant)
if (player.isAlive()) {
holder.textViewStatus.setText("✅ Vivant");
holder.textViewStatus.setTextColor(
holder.itemView.getContext().getColor(R.color.success_green)
);
} else {
holder.textViewStatus.setText("❌ Éliminé");
holder.textViewStatus.setTextColor(
holder.itemView.getContext().getColor(R.color.error_red)
);
}
// Avatar avec première lettre (vérification null et bounds)
String name = player.getName();
if (name != null && !name.isEmpty()) {
holder.playerAvatar.setText(name.substring(0, 1).toUpperCase());
} else {
holder.playerAvatar.setText("?");
}
// Couleur de fond selon le rôle
int backgroundColor;
int roleColor;
if (player.isCivil()) {
backgroundColor = holder.itemView.getContext().getColor(R.color.civil_bg_light);
roleColor = holder.itemView.getContext().getColor(R.color.civil_bg);
} else if (player.isUndercover()) {
backgroundColor = holder.itemView.getContext().getColor(R.color.undercover_bg_light);
roleColor = holder.itemView.getContext().getColor(R.color.undercover_bg);
} else {
backgroundColor = holder.itemView.getContext().getColor(R.color.mr_white_bg_light);
roleColor = holder.itemView.getContext().getColor(R.color.mr_white_bg);
}
// Définir la couleur de fond de la carte
if (holder.cardView != null) {
holder.cardView.setCardBackgroundColor(backgroundColor);
}
// Définir la couleur du texte du rôle
holder.textViewRole.setTextColor(roleColor);
}
@Override
public int getItemCount() {
return players.size();
}
static class ResultViewHolder extends RecyclerView.ViewHolder {
TextView playerAvatar;
TextView textViewPlayerName;
TextView textViewRole;
TextView textViewStatus;
com.google.android.material.card.MaterialCardView cardView;
public ResultViewHolder(@NonNull View itemView) {
super(itemView);
cardView = (com.google.android.material.card.MaterialCardView) itemView;
playerAvatar = itemView.findViewById(R.id.playerAvatarTextView);
textViewPlayerName = itemView.findViewById(R.id.playerNameTextView);
textViewRole = itemView.findViewById(R.id.playerRoleTextView);
textViewStatus = itemView.findViewById(R.id.playerStatusBadge);
}
}
}
@@ -0,0 +1,257 @@
package com.example.boidelov3.games.papelito;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.boidelov3.R;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.google.android.material.textfield.TextInputEditText;
import java.util.ArrayList;
import java.util.List;
/**
* Activity de configuration pour le jeu Papelito (Undercover)
*/
public class PapelitoSetupActivity extends AppCompatActivity {
private static final int MIN_PLAYERS = 3;
private static final int MAX_PLAYERS = 12;
private static final int MIN_UNDERCOVERS = 1;
private static final int MAX_UNDERCOVERS = 3;
private LinearLayout playersContainer;
private MaterialButton addPlayerButton;
private MaterialButton startGameButton;
private SeekBar undercoverSeekBar;
private TextView undercoverText;
private MaterialSwitch mrWhiteSwitch;
private MaterialToolbar toolbar;
private static final int DEFAULT_DISCUSSION_TIME_SECONDS = 120; // 2 minutes par défaut
private final List<String> playerNames = new ArrayList<>();
private int undercoverCount = 1;
private boolean mrWhiteEnabled = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_papelito_setup);
initViews();
setupToolbar();
setupListeners();
// Ajouter 3 joueurs par défaut
addPlayerRow();
addPlayerRow();
addPlayerRow();
updateUndercoverText();
updatePlayerNames();
}
private void initViews() {
toolbar = findViewById(R.id.toolbar);
playersContainer = findViewById(R.id.playersContainer);
addPlayerButton = findViewById(R.id.addPlayerButton);
startGameButton = findViewById(R.id.startGameButton);
undercoverSeekBar = findViewById(R.id.undercoverSeekBar);
undercoverText = findViewById(R.id.undercoverText);
mrWhiteSwitch = findViewById(R.id.mrWhiteSwitch);
}
private void setupToolbar() {
toolbar.setNavigationOnClickListener(v -> finish());
}
private void setupListeners() {
addPlayerButton.setOnClickListener(v -> {
if (playerNames.size() < MAX_PLAYERS) {
addPlayerRow();
} else {
Toast.makeText(this, "Maximum " + MAX_PLAYERS + " joueurs", Toast.LENGTH_SHORT).show();
}
});
startGameButton.setOnClickListener(v -> startGame());
undercoverSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
undercoverCount = progress + 1; // +1 car le SeekBar commence à 0
updateUndercoverText();
updateMaxUndercoverLimit();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
mrWhiteSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
mrWhiteEnabled = isChecked;
if (isChecked) {
// Vérifier qu'on a assez de joueurs pour Mr White
int requiredPlayers = undercoverCount + 2; // Au moins 2 civils
if (playerNames.size() < requiredPlayers) {
Toast.makeText(this,
"Mr White nécessite au moins " + requiredPlayers + " joueurs",
Toast.LENGTH_SHORT).show();
mrWhiteSwitch.setChecked(false);
mrWhiteEnabled = false;
}
}
});
}
private void addPlayerRow() {
View playerRow = LayoutInflater.from(this).inflate(R.layout.item_player_row, playersContainer, false);
TextInputEditText playerNameEdit = playerRow.findViewById(R.id.playerName);
MaterialButton removeButton = playerRow.findViewById(R.id.removePlayerButton);
TextView playerNumber = playerRow.findViewById(R.id.playerNumber);
int position = playersContainer.getChildCount();
playerNumber.setText(String.valueOf(position + 1));
// Cacher le bouton de suppression pour les 3 premiers joueurs (minimum requis)
if (position < MIN_PLAYERS) {
removeButton.setVisibility(View.GONE);
}
removeButton.setOnClickListener(v -> {
if (playersContainer.getChildCount() > MIN_PLAYERS) {
playersContainer.removeView(playerRow);
updatePlayerNumbers();
updatePlayerNames();
updateMaxUndercoverLimit();
} else {
Toast.makeText(this, "Minimum " + MIN_PLAYERS + " joueurs", Toast.LENGTH_SHORT).show();
}
});
playerNameEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
updatePlayerNames();
}
});
playersContainer.addView(playerRow);
}
private void updatePlayerNumbers() {
for (int i = 0; i < playersContainer.getChildCount(); i++) {
View row = playersContainer.getChildAt(i);
TextView playerNumber = row.findViewById(R.id.playerNumber);
playerNumber.setText(String.valueOf(i + 1));
// Afficher le bouton de suppression uniquement au-delà du minimum
MaterialButton removeButton = row.findViewById(R.id.removePlayerButton);
if (i >= MIN_PLAYERS) {
removeButton.setVisibility(View.VISIBLE);
} else {
removeButton.setVisibility(View.GONE);
}
}
}
private void updatePlayerNames() {
playerNames.clear();
for (int i = 0; i < playersContainer.getChildCount(); i++) {
View row = playersContainer.getChildAt(i);
TextInputEditText edit = row.findViewById(R.id.playerName);
String name = edit.getText().toString().trim();
if (!TextUtils.isEmpty(name)) {
playerNames.add(name);
} else {
playerNames.add("Joueur " + (i + 1));
}
}
updateStartButton();
updateMaxUndercoverLimit();
}
/**
* Met à jour la limite maximale d'undercovers selon le nombre de joueurs
* Il faut toujours au moins 2 civils (plus Mr White si activé)
*/
private void updateMaxUndercoverLimit() {
int playerCount = playerNames.size();
int maxAllowed;
if (mrWhiteEnabled) {
// Avec Mr White: max = joueurs - 2 (Mr White + 1 civil minimum)
maxAllowed = Math.max(MIN_UNDERCOVERS, playerCount - 2);
} else {
// Sans Mr White: max = joueurs - 2 (2 civils minimum)
maxAllowed = Math.max(MIN_UNDERCOVERS, playerCount - 2);
}
// Ajuster si nécessaire
if (undercoverCount > maxAllowed) {
undercoverCount = maxAllowed;
undercoverSeekBar.setProgress(undercoverCount - 1);
updateUndercoverText();
}
// Mettre à jour le max du SeekBar
int seekBarMax = Math.min(MAX_UNDERCOVERS, maxAllowed);
undercoverSeekBar.setMax(seekBarMax - 1); // -1 car le SeekBar commence à 0
}
private void updateUndercoverText() {
String text = undercoverCount + " undercover" + (undercoverCount > 1 ? "s" : "");
undercoverText.setText(text);
}
private void updateStartButton() {
int validPlayers = playerNames.size();
boolean canStart = validPlayers >= MIN_PLAYERS;
startGameButton.setEnabled(canStart);
startGameButton.setText(canStart ? "JOUER (" + validPlayers + ")" : "Ajoutez des joueurs");
}
private void startGame() {
updatePlayerNames();
if (playerNames.size() < MIN_PLAYERS) {
Toast.makeText(this, "Minimum " + MIN_PLAYERS + " joueurs requis", Toast.LENGTH_SHORT).show();
return;
}
// Vérifier qu'on a assez de joueurs pour la configuration
int requiredPlayers = undercoverCount + 2; // Au moins 2 civils
if (mrWhiteEnabled) {
requiredPlayers++; // +1 pour Mr White
}
if (playerNames.size() < requiredPlayers) {
Toast.makeText(this,
"Configuration invalide: il faut au moins " + requiredPlayers + " joueurs",
Toast.LENGTH_SHORT).show();
return;
}
// Lancer l'activité de jeu
Intent intent = new Intent(this, PapelitoGameActivity.class);
intent.putStringArrayListExtra("PLAYERS", new ArrayList<>(playerNames));
intent.putExtra("UNDERCOVER_COUNT", undercoverCount);
intent.putExtra("MR_WHITE_ENABLED", mrWhiteEnabled);
intent.putExtra("DISCUSSION_TIME", DEFAULT_DISCUSSION_TIME_SECONDS);
startActivity(intent);
}
}
@@ -7,6 +7,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.boidelov3.R;
import com.example.boidelov3.games.boideloclassic.BoideloClassicSetupActivity;
import com.example.boidelov3.games.papelito.PapelitoSetupActivity;
import com.example.boidelov3.hub.adapter.GameAdapter;
import com.example.boidelov3.hub.model.GameInfo;
import java.util.ArrayList;
@@ -65,13 +66,13 @@ public class GameSelectionActivity extends AppCompatActivity implements GameAdap
true // Available now
));
// Undercover - Jeu de déduction
// Papelito (Undercover) - Jeu de déduction
gamesList.add(new GameInfo(
"Undercover",
"Papelito",
"Trouvez l'undercover avant qu'il ne soit trop tard!",
R.drawable.ic_undercover,
R.drawable.ic_papelito,
GameInfo.GameType.UNDERCOVER,
false // Coming soon
true // Available now
));
// Jeux de règles
@@ -111,7 +112,7 @@ public class GameSelectionActivity extends AppCompatActivity implements GameAdap
startActivity(new Intent(this, com.example.boidelov3.games.game89.Game89SetupActivity.class));
break;
case UNDERCOVER:
// TODO: Implémenter UndercoverSetupActivity
startActivity(new Intent(this, com.example.boidelov3.games.papelito.PapelitoSetupActivity.class));
break;
case RULES:
// TODO: Implémenter RulesListActivity
@@ -84,6 +84,9 @@ public class SecureConfig {
* @return La clé API ou null si non trouvée
*/
public String getApiKey(String provider) {
if (provider == null || provider.trim().isEmpty()) {
return null;
}
String key = getPrefKeyForProvider(provider);
return sharedPreferences.getString(key, null);
}
@@ -95,6 +98,11 @@ public class SecureConfig {
* @return true si supprimée avec succès
*/
public boolean removeApiKey(String provider) {
if (provider == null || provider.trim().isEmpty()) {
Log.w(TAG, "Provider null ou vide pour removeApiKey");
return false;
}
SharedPreferences.Editor editor = sharedPreferences.edit();
String key = getPrefKeyForProvider(provider);
editor.remove(key);
@@ -127,6 +135,9 @@ public class SecureConfig {
* @return true si une clé existe
*/
public boolean hasApiKey(String provider) {
if (provider == null || provider.trim().isEmpty()) {
return false;
}
String key = getPrefKeyForProvider(provider);
return sharedPreferences.contains(key) && sharedPreferences.getString(key, null) != null;
}
@@ -143,9 +154,14 @@ public class SecureConfig {
return false;
}
if (provider == null || provider.trim().isEmpty()) {
Log.w(TAG, "Provider null ou vide");
return false;
}
String trimmedKey = apiKey.trim();
switch (provider.toLowerCase()) {
switch (provider.toLowerCase().trim()) {
case "openai":
// Les clés OpenAI commencent par "sk-"
return trimmedKey.startsWith("sk-") && trimmedKey.length() >= 20;
@@ -198,7 +214,10 @@ public class SecureConfig {
* Retourne la clé SharedPreferences appropriée selon le provider
*/
private String getPrefKeyForProvider(String provider) {
switch (provider.toLowerCase()) {
if (provider == null) {
return KEY_API_KEY; // Default to OpenAI key
}
switch (provider.toLowerCase().trim()) {
case "openrouter":
return KEY_API_KEY_OPENROUTER;
case "zai":