Relecture + Ajustement

This commit is contained in:
2026-03-29 20:41:19 +02:00
parent 1692cd0ba3
commit a7deb9f726
9 changed files with 186 additions and 68 deletions
+23 -15
View File
@@ -7,13 +7,12 @@ import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class Controleur {
public class Controleur implements View.OnClickListener {
private final Activity activite;
private final EtatJeu etatJeu;
private final VueGrille vueGrille;
private final long graine;
private final TextView tvScore;
private final TextView tvCoups;
@@ -25,14 +24,11 @@ public class Controleur {
private TextView tvNbObjectifs;
private TextView tvCles;
private Button btnReinitialisationObjectif;
private View layoutCles;
private View layoutCles;
public Controleur(Activity activite, EtatJeu etatJeu, VueGrille vueGrille,
long graine, TextView tvScore, TextView tvCoups,
boolean hardMode) {
this.activite = activite;
this.etatJeu = etatJeu;
this.vueGrille = vueGrille;
@@ -49,17 +45,21 @@ public class Controleur {
this.layoutCles = activite.findViewById(R.id.layoutCles);
this.btnReinitialisationObjectif = activite.findViewById(R.id.btnReinitialisationObjectif);
if (tvObjectif != null) tvObjectif.setVisibility(View.VISIBLE);
if (tvNbObjectifs != null) tvNbObjectifs.setVisibility(View.VISIBLE);
if (layoutCles != null) layoutCles.setVisibility(View.VISIBLE);
if (tvObjectif != null) {
tvObjectif.setVisibility(View.VISIBLE);
}
if (tvNbObjectifs != null) {
tvNbObjectifs.setVisibility(View.VISIBLE);
}
if (layoutCles != null) {
layoutCles.setVisibility(View.VISIBLE);
}
if (btnReinitialisationObjectif != null) {
btnReinitialisationObjectif.setOnClickListener(v -> reinitialiserObjectif());
btnReinitialisationObjectif.setOnClickListener(this);
}
}
rafraichirAffichage();
}
@@ -86,8 +86,8 @@ public class Controleur {
*/
public void gererFinDeCoup() {
// 1 clé tous les 5 coups
if (hardMode && etatJeu.obtenirNbCoups() % 5 == 0) {
// 1 clé tous les 4 coups
if (hardMode && etatJeu.obtenirNbCoups() % 4 == 0) {
gestionnaireObjectifs.ajouterCle();
}
// --- Logique Hard Mode
@@ -120,7 +120,7 @@ public class Controleur {
}
/**
* Tente de réinitialiser l'objectif en cours en consommant 3 clés.
* Réinitialiser l'objectif en cours si la clé est consommé.
* Affiche un Toast si le joueur n'a pas assez de clés.
*/
public void reinitialiserObjectif() {
@@ -167,6 +167,14 @@ public class Controleur {
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btnReinitialisationObjectif) {
reinitialiserObjectif();
}
}
public void rafraichirAffichage() {
tvScore.setText("Score : " + etatJeu.obtenirScore());
tvCoups.setText("Coups : " + etatJeu.obtenirNbCoups());
+36 -3
View File
@@ -6,15 +6,13 @@ import java.util.Random;
/**
* Gère la session de jeu (score, coups, état de la partie) et délègue la
* manipulation
* physique de la grille à la classe Plateau.
* manipulation des pièces de la grille à la classe Plateau.
*/
public class EtatJeu {
private final Plateau plateau;
private final Random aleatoire;
private final boolean hardMode;
private int score = 0;
private int nbCoups = 0;
private boolean partieTerminee = false;
@@ -49,6 +47,11 @@ public class EtatJeu {
return plateau.copierGrille();
}
/**
* Retourne l'état des verrous (cases bloquées)
* On crée une copie pour que personne ne puisse modifier les verrous
* directement sans passer par les règles de cette classe
*/
public boolean[][] obtenirVerrous() {
// Copie manuelle pour respecter l'encapsulation
boolean[][] v = plateau.getVerrous();
@@ -61,6 +64,10 @@ public class EtatJeu {
// --- Actions
/**
* Tente de déplacer une ligne ou une colonne.
* Si le mouvement ne crée aucune série de 3 couleurs, il est annulé.
*/
public boolean appliquerCoup(boolean estLigne, int index, int sens) {
if (partieTerminee || plateau.estBloque(estLigne, index)) {
return false;
@@ -93,6 +100,10 @@ public class EtatJeu {
return true;
}
/**
* Gère les "cascades" : quand des pièces disparaissent, d'autres tombent
* et peuvent créer de nouvelles séries automatiquement.
*/
private int resoudreCascades() {
int baseTotal = 0;
int nbSeriesTotal = 0;
@@ -176,6 +187,9 @@ public class EtatJeu {
return new int[] { pts, count };
}
/**
* Définit combien de points rapporte une série selon sa taille (3, 4, 5...).
*/
private int pointsPourLongueur(int n) {
if (n == 3)
return 8;
@@ -186,6 +200,10 @@ public class EtatJeu {
return 64;
}
/**
* Transforme une liste de positions en une "carte" (masque) de la grille
* pour savoir rapidement quelles cases doivent être supprimées.
*/
private boolean[][] creerMasque(List<int[]> positions) {
boolean[][] m = new boolean[Plateau.NB_LIGNES][Plateau.NB_COLONNES];
for (int[] p : positions) {
@@ -200,6 +218,10 @@ public class EtatJeu {
}
}
/**
* Parcourt l'intégralité des possibilités pour vérifier si le joueur
* peut encore effectuer au moins un mouvement créant une série.
*/
public boolean aUnCoupValide() {
for (int i = 0; i < Plateau.NB_LIGNES; i++) {
if (plateau.estBloque(true, i)) {
@@ -224,6 +246,10 @@ public class EtatJeu {
return false;
}
/**
* Simule un déplacement pour voir s'il créerait une série,
* sans modifier la "vraie" grille de jeu du joueur.
*/
private boolean simulerCoup(boolean estLigne, int idx, int s) {
int[][] save = plateau.copierGrille();
if (estLigne) {
@@ -236,10 +262,14 @@ public class EtatJeu {
return ok;
}
public void forcerFinDePartie() {
partieTerminee = true;
}
/**
* Prépare les données du jeu pour les sauvegarder (en cas de fermeture de l'app).
*/
public void sauvegarderEtat(Bundle b) {
int[] g = new int[36];
boolean[] v = new boolean[36];
@@ -258,6 +288,9 @@ public class EtatJeu {
b.putBoolean("partieTerminee", partieTerminee);
}
/**
* Recharge les données du jeu sauvegardées précédemment.
*/
public void restaurerEtat(Bundle b) {
if (!b.containsKey("grille")) return;
@@ -29,7 +29,7 @@ public class GestionnaireObjectifs {
// N (nbSeriesCible) tel que M - N >= 1 et N <= 3.
for (int m = 2; m <= 5; m++) {
for (int n = 1; n <= m - 1 && n <= 3; n++) {
// Probabilité inversement proportionnelle à la facilité (M-N).
// Plus l'objectif offre une marge de coups importante (M-N), plus il a de chances de tomber.
// Plus (M-N) est petit, plus c'est dur.
int diff = m - n;
int poids = diff * diff; // On utilise le carré pour accentuer la différence de probabilité
@@ -99,4 +99,3 @@ public class GestionnaireObjectifs {
}
}
}
@@ -3,21 +3,28 @@ package sae.chuzzle;
import android.view.MotionEvent;
import android.view.View;
/**
* Gère les interactions tactiles sur la grille de jeu.
* Traduit les mouvements de doigt en déplacements de lignes ou colonnes.
*/
public class GestionnaireTactile implements View.OnTouchListener {
private static final int NB_LIGNES = Plateau.NB_LIGNES;
private static final int NB_COLONNES = Plateau.NB_COLONNES;
// Distance minimale (en pixels) avant de décider si le joueur déplace une ligne ou une colonne.
// Cela évite les déclenchements accidentels lors d'un simple clic.
private static final float SEUIL_DIRECTION_PX = 10f;
private final VueGrille vueGrille;
private final EtatJeu etatJeu;
private final Controleur controleur;
private float touchDebutX, touchDebutY;
private int ligneTouchee, colonneTouchee;
private Boolean estLigne;
private int indexGlissement;
private float decalagePx;
private float touchDebutX, touchDebutY; // Position initiale du doigt
private int ligneTouchee, colonneTouchee; // Indices de la case située sous le doigt au départ
private Boolean estLigne; // null = direction inconnue, true = horizontal, false = vertical
private int indexGlissement; // Numéro de la ligne ou colonne que l'on fait glisser
private float decalagePx; // Distance parcourue par le doigt depuis le début
private float tailleCase;
public GestionnaireTactile(VueGrille vueGrille,
@@ -61,12 +68,15 @@ public class GestionnaireTactile implements View.OnTouchListener {
estLigne = null;
decalagePx = 0f;
// Calcule le décalage pour que le doigt vise bien les cases centrées
float margeGauche = (vueGrille.getWidth() - tailleCase * NB_COLONNES) / 2f;
float margeHaut = (vueGrille.getHeight() - tailleCase * NB_LIGNES) / 2f;
// Conversion des coordonnées pixels en indices de tableau (0 à 5)
ligneTouchee = (int) ((touchDebutY - margeHaut) / tailleCase);
colonneTouchee = (int) ((touchDebutX - margeGauche) / tailleCase);
// Sécurité pour éviter de sortir des limites du tableau si on touche les bords
ligneTouchee = Math.max(0, Math.min(NB_LIGNES - 1, ligneTouchee));
colonneTouchee = Math.max(0, Math.min(NB_COLONNES - 1, colonneTouchee));
}
@@ -75,8 +85,10 @@ public class GestionnaireTactile implements View.OnTouchListener {
float dx = event.getX() - touchDebutX;
float dy = event.getY() - touchDebutY;
// on détermine si le joueur glisse vers le côté ou vers le haut/bas
if (estLigne == null) {
if (Math.abs(dx) > SEUIL_DIRECTION_PX || Math.abs(dy) > SEUIL_DIRECTION_PX) {
// On verrouille l'axe : celui qui a la plus grande distance parcourue gagne
estLigne = Math.abs(dx) >= Math.abs(dy);
if (estLigne) {
@@ -13,8 +13,6 @@ public class MainActivity extends Activity {
private Button btnMenu;
private VueGrille vueGrille;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -23,7 +21,6 @@ public class MainActivity extends Activity {
// --- Modèle
long graine = getIntent().getLongExtra("graine", System.currentTimeMillis());
hardMode = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE).getBoolean("hard_mode", false);
etatJeu = new EtatJeu(graine, hardMode);
+38 -1
View File
@@ -21,6 +21,10 @@ public class Plateau {
initialiserGrilleSansTriples();
}
/**
* Remplit la grille aléatoirement au début de la partie.
* On s'assure qu'aucune série de 3 n'est créée automatiquement au démarrage.
*/
private void initialiserGrilleSansTriples() {
for (int l = 0; l < NB_LIGNES; l++) {
for (int c = 0; c < NB_COLONNES; c++) {
@@ -33,6 +37,9 @@ public class Plateau {
}
}
/**
* Vérifie si placer une couleur à une position précise créerait un alignement de 3.
*/
private boolean creeTriple(int l, int c, int val) {
if (c >= 2 && grille[l][c - 1] == val && grille[l][c - 2] == val) {
return true;
@@ -43,6 +50,10 @@ public class Plateau {
return false;
}
/**
* Fait défiler une colonne horizontalement. C'est un mouvement circulaire :
* les cases qui sortent par la gauche reviennent par la droite (comme un rouleau)
*/
public void decalerLigne(int ligne, int sens) {
int s = ((sens % NB_COLONNES) + NB_COLONNES) % NB_COLONNES;
for (int etape = 0; etape < s; etape++) {
@@ -54,6 +65,10 @@ public class Plateau {
}
}
/**
* Fait défiler une colonne verticalement. C'est un mouvement circulaire :
* les cases qui sortent par le bas reviennent par le haut.
*/
public void decalerColonne(int col, int sens) {
int s = ((sens % NB_LIGNES) + NB_LIGNES) % NB_LIGNES;
for (int etape = 0; etape < s; etape++) {
@@ -65,6 +80,10 @@ public class Plateau {
}
}
/**
* Parcourt toute la grille pour identifier les alignements de 3 couleurs ou plus.
* Retourne nne liste de coordonnées [ligne, colonne] des cases à supprimer.
*/
public List<int[]> trouverSeries() {
boolean[][] aSupprimer = new boolean[NB_LIGNES][NB_COLONNES];
// Analyse horizontale
@@ -112,6 +131,10 @@ public class Plateau {
return positions;
}
/**
* Vérifie si une ligne ou une colonne contient au moins une case verrouillée.
* Si c'est le cas, elle ne peut pas être déplacée par le joueur.
*/
public boolean estBloque(boolean estLigne, int index) {
if (estLigne) {
for (int c = 0; c < NB_COLONNES; c++) {
@@ -129,6 +152,10 @@ public class Plateau {
return false;
}
/**
* Ajoute un nouveau verrou sur une case libre au hasard.
* La fréquence d'apparition augmente au fur et à mesure que la partie avance.
*/
public void ajouterVerrou(int nbCoups) {
int intervalle = Math.max(1, 5 - nbCoups / 10);
if (nbCoups % intervalle != 0) {
@@ -149,6 +176,9 @@ public class Plateau {
verrous[choix[0]][choix[1]] = true;
}
/**
* Libère les verrous sur les cases qui viennent d'être complétées dans une série.
*/
public void libererVerrous(boolean[][] masque) {
for (int l = 0; l < NB_LIGNES; l++) {
for (int c = 0; c < NB_COLONNES; c++) {
@@ -159,6 +189,9 @@ public class Plateau {
}
}
/**
* Création d'un double de la grille actuelle (utile pour tester des mouvements).
*/
public int[][] copierGrille() {
int[][] copie = new int[NB_LIGNES][NB_COLONNES];
for (int l = 0; l < NB_LIGNES; l++) {
@@ -180,7 +213,11 @@ public class Plateau {
public boolean[][] getVerrous() {
return verrous;
}
/**
* Logique de gravité : les cases vides (supprimées) sont comblées par les cases du dessus.
* De nouvelles couleurs sont générées au sommet du plateau.
*/
public void faireTomber(boolean[][] aSupprimer) {
for (int col = 0; col < NB_COLONNES; col++) {
List<Integer> survivants = new ArrayList<>();
@@ -8,11 +8,10 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class SeedActivity extends Activity implements View.OnClickListener {
public class SeedActivity extends Activity {
private EditText etGraine;
private Button btnJouer;
private Button btnRetour;
@Override
@@ -23,43 +22,18 @@ public class SeedActivity extends Activity implements View.OnClickListener {
etGraine = findViewById(R.id.etGraine);
btnJouer = findViewById(R.id.btnJouer);
btnRetour = findViewById(R.id.btnBack);
btnRetour.setOnClickListener(this);
btnJouer.setOnClickListener(this);
SeedControleur controleur = new SeedControleur(this, etGraine);
btnRetour.setOnClickListener(controleur);
btnJouer.setOnClickListener(controleur);
}
@Override
public void onClick(View v) {
if (v == btnJouer) {
lancerPartieAvecGraine();
}
if (v == btnRetour) {
Intent intent = new Intent(this, MenuActivity.class);
startActivity(intent);
}
}
private void lancerPartieAvecGraine() {
String texte = etGraine.getText().toString().trim();
if (texte.isEmpty()) {
Toast.makeText(this, "Veuillez entrer une graine.",
Toast.LENGTH_SHORT
).show();
return;
}
long graine;
try {
graine = Long.parseLong(texte);
} catch (NumberFormatException e) {
graine = texte.hashCode();
}
public void lancerPartie(long graine) {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("graine", graine);
startActivity(intent);
}
public void retourMenu() {
Intent intent = new Intent(this, MenuActivity.class);
startActivity(intent);
}
}
@@ -0,0 +1,53 @@
package sae.chuzzle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
/**
* Contrôleur pour la saisie de la graine.
* Gère la validation de la saisie et la navigation.
*/
public class SeedControleur implements View.OnClickListener {
private final SeedActivity activite;
private final EditText etGraine;
public SeedControleur(SeedActivity activite, EditText etGraine) {
this.activite = activite;
this.etGraine = etGraine;
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btnJouer) {
traiterGraine();
} else if (id == R.id.btnBack) {
activite.retourMenu();
}
}
/**
* Valide le texte saisi et demande à l'activité de lancer la partie.
*/
private void traiterGraine() {
String texte = etGraine.getText().toString().trim();
if (texte.isEmpty()) {
Toast.makeText(activite, "Veuillez entrer une graine.", Toast.LENGTH_SHORT).show();
return;
}
long graine;
try {
graine = Long.parseLong(texte);
} catch (NumberFormatException e) {
graine = texte.hashCode();
}
activite.lancerPartie(graine);
}
}
+9 -4
View File
@@ -19,7 +19,7 @@ public class VueGrille extends View {
private static final int NB_COLONNES = 6;
private static final int NB_TYPES = 7;
/** Symboles pour le mode daltonien, un par type. */
/** Symboles pour les daltoniens, un par type. */
private static final String[] SYMBOLES = {"", "", "", "", "", "", ""};
// Données nécessaires au dessin
@@ -33,8 +33,8 @@ public class VueGrille extends View {
// Outils de dessin
private final Paint pinceauCase = new Paint();
private final Paint pinceauSymbole = new Paint();
private final Paint pinceauCase = new Paint();
private final Paint pinceauSymbole = new Paint();
private final Paint pinceauSelection = new Paint();
private Bitmap imageChaine;
@@ -130,14 +130,18 @@ public class VueGrille extends View {
pinceauSymbole.setTextSize(tailleCase * 0.4f);
// On parcourt chaque case de la grille pour la dessiner
for (int ligne = 0; ligne < NB_LIGNES; ligne++) {
for (int colonne = 0; colonne < NB_COLONNES; colonne++) {
float offsetX = 0f;
float offsetY = 0f;
// Si un glissement est en cours, on calcule le décalage visuel
if (animEstLigne != null) {
// On convertit les pixels du doigt en nombre de cases de décalage
int nbCases = Math.round(animDecalagePx / tailleCase);
// On applique le décalage uniquement à la ligne ou colonne que le joueur bouge
if (animEstLigne && ligne == animIndex) {
offsetX = nbCases * tailleCase;
} else if (!animEstLigne && colonne == animIndex) {
@@ -150,6 +154,7 @@ public class VueGrille extends View {
float x2 = margeGauche + (colonne + 1) * tailleCase - 6 + offsetX;
float y2 = margeHaut + (ligne + 1) * tailleCase - 6 + offsetY;
// On dessine la case (et ses éventuelles copies pour l'effet de boucle)
dessinerCase(canvas, ligne, colonne, x1, y1, x2, y2,
tailleCase, margeGauche, margeHaut, offsetX, offsetY);
}
@@ -170,7 +175,7 @@ public class VueGrille extends View {
// Dessin case principale
dessinerRectCase(canvas, ligne, colonne, x1, y1, x2, y2);
// Réapparition de l'autre côté (wrap)
// Réapparition de l'autre côté (effet boucle infini)
if (offsetX != 0f) {
float bordD = margeGauche + largeurGrille;
float bordG = margeGauche;