diff --git a/src/main/java/sae/chuzzle/MainActivity.java b/src/main/java/sae/chuzzle/MainActivity.java
index 3269e09..1e48cd3 100644
--- a/src/main/java/sae/chuzzle/MainActivity.java
+++ b/src/main/java/sae/chuzzle/MainActivity.java
@@ -1,6 +1,7 @@
package sae.chuzzle;
import android.app.Activity;
+import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
@@ -10,8 +11,15 @@ import android.widget.TextView;
public class MainActivity extends Activity implements View.OnClickListener {
+
+
private Controleur controleur;
private Button btnJouer;
+ private Button btnMenu;
+
+
+ private boolean hardMode;
+ private EtatJeu etatJeu;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -19,33 +27,79 @@ public class MainActivity extends Activity implements View.OnClickListener {
setContentView(R.layout.activity_main);
// --- Modèle ---
- long graine = getIntent().getLongExtra("graine", System.currentTimeMillis());
- boolean hardMode = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE)
+ long graine = getIntent().getLongExtra("graine", System.currentTimeMillis());
+ hardMode = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE)
.getBoolean("hard_mode", false);
boolean daltonien = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE)
.getBoolean("daltonien", false);
- EtatJeu etatJeu = new EtatJeu(graine, hardMode);
+ etatJeu = new EtatJeu(graine, hardMode);
+
+ // --- Restauration si retour de pause ---
+ if (savedInstanceState != null) {
+ etatJeu.restaurerEtat(savedInstanceState);
+ }
// --- Vue ---
VueGrille vueGrille = findViewById(R.id.vueGrille);
vueGrille.definirModeDaltonien(daltonien);
+
+
// --- Controleur ---
btnJouer = findViewById(R.id.btnJouer);
btnJouer.setOnClickListener(this);
+ btnMenu = findViewById(R.id.btnMenu);
+ btnMenu.setOnClickListener(this);
+
+
controleur = new Controleur(
this,
etatJeu,
vueGrille,
- (TextView) findViewById(R.id.tvScore),
- (TextView) findViewById(R.id.tvCoups),
+ graine,
+ (TextView) findViewById(R.id.tvScore),
+ (TextView) findViewById(R.id.tvCoups),
(RadioButton) findViewById(R.id.rbLigne),
(RadioButton) findViewById(R.id.rbDroite),
- (Spinner) findViewById(R.id.spinnerIndex),
- btnJouer
+ (Spinner) findViewById(R.id.spinnerIndex),
+ btnJouer,
+ hardMode
);
+
+ // --- Gestion tactile ---
+ GestionnaireTactile gestionnaireTactile =
+ new GestionnaireTactile(vueGrille, etatJeu, controleur);
+ vueGrille.setOnTouchListener(gestionnaireTactile);
+
+ // --- Logique Hard mode ---
+ if (hardMode) {
+ // TODO: Ajouter la nouvelle fonctionnalité pour le hard mode ici (en remplacement du timer)
+ }
+ }
+
+ // -
+ //
+
+ // -
+ // CYCLE DE VIE
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (etatJeu != null) {
+ etatJeu.sauvegarderEtat(outState);
+ }
+ if (controleur != null) {
+ controleur.sauvegarderEtat(outState);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
}
@Override
@@ -53,5 +107,10 @@ public class MainActivity extends Activity implements View.OnClickListener {
if (v == btnJouer) {
controleur.gererCoupJoueur();
}
+ if (v == btnMenu) {
+ etatJeu.forcerFinDePartie();
+ Intent intent = new Intent(this, MenuActivity.class);
+ startActivity(intent);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/src.zip b/src/src.zip
new file mode 100644
index 0000000..28b9cec
Binary files /dev/null and b/src/src.zip differ
diff --git a/src/src/androidTest/java/sae/chuzzle/ExampleInstrumentedTest.java b/src/src/androidTest/java/sae/chuzzle/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..dc7075e
--- /dev/null
+++ b/src/src/androidTest/java/sae/chuzzle/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package sae.chuzzle;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("sae.chuzzle", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/AndroidManifest.xml b/src/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..34cdb93
--- /dev/null
+++ b/src/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/java/sae/chuzzle/Controleur.java b/src/src/main/java/sae/chuzzle/Controleur.java
new file mode 100644
index 0000000..62d5650
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/Controleur.java
@@ -0,0 +1,133 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.RadioButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.List;
+
+public class Controleur {
+
+ private final Activity activite;
+ private final EtatJeu etatJeu;
+ private final VueGrille vueGrille;
+ private final long graine;
+
+ private final TextView tvScore;
+ private final TextView tvCoups;
+ private final RadioButton rbLigne;
+ private final RadioButton rbDroite;
+ private final Spinner spinnerIndex;
+ private final Button btnJouer;
+
+ // Hard Mode
+ private final boolean hardMode;
+
+ // -
+ // CONSTRUCTEUR
+
+
+ public Controleur(Activity activite, EtatJeu etatJeu, VueGrille vueGrille,
+ long graine,
+ TextView tvScore, TextView tvCoups,
+ RadioButton rbLigne, RadioButton rbDroite,
+ Spinner spinnerIndex, Button btnJouer,
+ boolean hardMode) {
+
+ this.activite = activite;
+ this.etatJeu = etatJeu;
+ this.vueGrille = vueGrille;
+ this.graine = graine;
+ this.tvScore = tvScore;
+ this.tvCoups = tvCoups;
+ this.rbLigne = rbLigne;
+ this.rbDroite = rbDroite;
+ this.spinnerIndex = spinnerIndex;
+ this.btnJouer = btnJouer;
+ this.hardMode = hardMode;
+
+ initialiserSpinner();
+ rafraichirAffichage();
+ }
+
+ // -
+ // INITIALISATION
+
+
+ private void initialiserSpinner() {
+ String[] indices = {"0", "1", "2", "3", "4", "5"};
+ ArrayAdapter adaptateur = new ArrayAdapter<>(
+ activite,
+ android.R.layout.simple_spinner_item,
+ indices
+ );
+ adaptateur.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spinnerIndex.setAdapter(adaptateur);
+ }
+
+ // -
+ // GESTION DU COUP
+
+
+ public void gererCoupJoueur() {
+ boolean estLigne = rbLigne.isChecked();
+ int index = spinnerIndex.getSelectedItemPosition();
+
+ int sens;
+ if (rbDroite.isChecked()) {
+ sens = +1;
+ } else {
+ sens = -1;
+ }
+
+ boolean accepte = etatJeu.appliquerCoup(estLigne, index, sens);
+
+ if (!accepte) {
+ Toast.makeText(activite, "Coup invalide !", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(activite, "Coup valide !", Toast.LENGTH_SHORT).show();
+ rafraichirAffichage();
+ verifierFinDePartie();
+ }
+
+ // -
+ // FIN DE PARTIE
+
+
+ public void verifierFinDePartie() {
+ if (etatJeu.estTerminee()) {
+ Intent intent = new Intent(activite, FinPartieActivity.class);
+ intent.putExtra("score", etatJeu.obtenirScore());
+ intent.putExtra("nbCoups", etatJeu.obtenirNbCoups());
+ intent.putExtra("graine", graine);
+ activite.startActivity(intent);
+ }
+ }
+
+ // -
+ // MISE A JOUR DE LA VUE
+
+
+ public void rafraichirAffichage() {
+ tvScore.setText("Score : " + etatJeu.obtenirScore());
+ tvCoups.setText("Coups : " + etatJeu.obtenirNbCoups());
+ vueGrille.definirGrille(etatJeu.obtenirGrille());
+ vueGrille.definirVerrous(etatJeu.obtenirVerrous());
+
+ if (etatJeu.estTerminee()) {
+ btnJouer.setEnabled(false);
+ }
+ }
+
+ public void sauvegarderEtat(Bundle out) {
+ // Nettoyé de la logique d'objectifs
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/java/sae/chuzzle/EtatJeu.java b/src/src/main/java/sae/chuzzle/EtatJeu.java
new file mode 100644
index 0000000..4d5f670
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/EtatJeu.java
@@ -0,0 +1,598 @@
+package sae.chuzzle;
+
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class EtatJeu {
+
+ public static final int NB_LIGNES = 6;
+ public static final int NB_COLONNES = 6;
+ public static final int NB_TYPES = 7;
+
+ private final int[][] grille = new int[NB_LIGNES][NB_COLONNES];
+ private final boolean[][] verrous = new boolean[NB_LIGNES][NB_COLONNES];
+ private final Random aleatoire;
+
+ private int score = 0;
+ private int nbCoups = 0;
+ private boolean partieTerminee = false;
+ private boolean hardMode = false;
+
+ //-
+ // CONSTRUCTEURS
+
+
+ public EtatJeu() {
+ aleatoire = new Random();
+ initialiserGrilleSansTriples();
+ }
+
+ public EtatJeu(long graine, boolean hardMode) {
+ aleatoire = new Random(graine);
+ this.hardMode = hardMode;
+ initialiserGrilleSansTriples();
+ }
+
+ // -
+ // GETTERS
+
+
+ public int obtenirScore() {
+ return score;
+ }
+
+ public int obtenirNbCoups() {
+ return nbCoups;
+ }
+
+ public boolean estTerminee() {
+ return partieTerminee;
+ }
+
+ // -
+ // SAUVEGARDE ET RESTAURATION
+
+ public void sauvegarderEtat(Bundle bundle) {
+ int[] flatGrille = new int[NB_LIGNES * NB_COLONNES];
+ boolean[] flatVerrous = new boolean[NB_LIGNES * NB_COLONNES];
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ for (int c = 0; c < NB_COLONNES; c++) {
+ flatGrille[l * NB_COLONNES + c] = grille[l][c];
+ flatVerrous[l * NB_COLONNES + c] = verrous[l][c];
+ }
+ }
+
+ bundle.putIntArray("grille", flatGrille);
+ bundle.putBooleanArray("verrous", flatVerrous);
+ bundle.putInt("score", score);
+ bundle.putInt("nbCoups", nbCoups);
+ bundle.putBoolean("partieTerminee", partieTerminee);
+ }
+
+ public void restaurerEtat(Bundle bundle) {
+ if (!bundle.containsKey("grille")) return;
+
+ int[] flatGrille = bundle.getIntArray("grille");
+ boolean[] flatVerrous = bundle.getBooleanArray("verrous");
+
+ if (flatGrille != null && flatVerrous != null) {
+ for (int l = 0; l < NB_LIGNES; l++) {
+ for (int c = 0; c < NB_COLONNES; c++) {
+ grille[l][c] = flatGrille[l * NB_COLONNES + c];
+ verrous[l][c] = flatVerrous[l * NB_COLONNES + c];
+ }
+ }
+ }
+
+ score = bundle.getInt("score");
+ nbCoups = bundle.getInt("nbCoups");
+ partieTerminee = bundle.getBoolean("partieTerminee");
+ }
+
+ public int obtenirCase(int ligne, int colonne) {
+ return grille[ligne][colonne];
+ }
+
+ public int[][] obtenirGrille() {
+ int[][] copie = new int[NB_LIGNES][NB_COLONNES];
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(grille[l], 0, copie[l], 0, NB_COLONNES);
+ }
+
+ return copie;
+ }
+
+ public boolean[][] obtenirVerrous() {
+ boolean[][] copie = new boolean[NB_LIGNES][NB_COLONNES];
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(verrous[l], 0, copie[l], 0, NB_COLONNES);
+ }
+
+ return copie;
+ }
+
+ // -
+ // INITIALISATION SANS TRIPLES
+ //
+
+ private void initialiserGrilleSansTriples() {
+ for (int ligne = 0; ligne < NB_LIGNES; ligne++) {
+ for (int colonne = 0; colonne < NB_COLONNES; colonne++) {
+
+ int valeur;
+ do {
+ valeur = aleatoire.nextInt(NB_TYPES);
+ } while (creeTriple(ligne, colonne, valeur));
+
+ grille[ligne][colonne] = valeur;
+ }
+ }
+ }
+
+ private boolean creeTriple(int ligne, int colonne, int valeur) {
+
+ if (colonne >= 2) {
+ if (grille[ligne][colonne - 1] == valeur &&
+ grille[ligne][colonne - 2] == valeur) {
+ return true;
+ }
+ }
+
+ if (ligne >= 2) {
+ if (grille[ligne - 1][colonne] == valeur &&
+ grille[ligne - 2][colonne] == valeur) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // -
+ // DECALAGE CIRCULAIRE
+
+
+ public void decalerLigne(int ligne, int sens) {
+ int s = ((sens % NB_COLONNES) + NB_COLONNES) % NB_COLONNES;
+
+ for (int etape = 0; etape < s; etape++) {
+ int tmp = grille[ligne][NB_COLONNES - 1];
+
+ for (int c = NB_COLONNES - 1; c > 0; c--) {
+ grille[ligne][c] = grille[ligne][c - 1];
+ }
+
+ grille[ligne][0] = tmp;
+ }
+ }
+
+ public void decalerColonne(int colonne, int sens) {
+ int s = ((sens % NB_LIGNES) + NB_LIGNES) % NB_LIGNES;
+
+ for (int etape = 0; etape < s; etape++) {
+ int tmp = grille[NB_LIGNES - 1][colonne];
+
+ for (int l = NB_LIGNES - 1; l > 0; l--) {
+ grille[l][colonne] = grille[l - 1][colonne];
+ }
+
+ grille[0][colonne] = tmp;
+ }
+ }
+
+ // -
+ // APPLIQUER UN COUP
+
+
+ public boolean appliquerCoup(boolean estLigne, int index, int sens) {
+ if (partieTerminee) {
+ return false;
+ }
+
+ // Les verrous bloquent toujours, quel que soit le mode
+ if (estBloque(estLigne, index)) {
+ return false;
+ }
+
+ int[][] sauvegarde = copierGrille();
+
+ if (estLigne) {
+ decalerLigne(index, sens);
+ } else {
+ decalerColonne(index, sens);
+ }
+
+ if (trouverSeries().isEmpty()) {
+ restaurerGrille(sauvegarde);
+ return false;
+ }
+
+ nbCoups++;
+ score += resoudreEtRemplir();
+
+ // Verrou après chaque coup ; 2 verrous en hard mode (plus difficile)
+ ajouterVerrou();
+ if (hardMode) {
+ ajouterVerrou();
+ }
+
+ if (!aUnCoupValide()) {
+ partieTerminee = true;
+ }
+
+ return true;
+ }
+
+ // -
+ // TROUVER LES SERIES
+
+
+ public List trouverSeries() {
+ boolean[][] aSupprimer = new boolean[NB_LIGNES][NB_COLONNES];
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ int c = 0;
+
+ while (c < NB_COLONNES) {
+ int type = grille[l][c];
+ int fin = c + 1;
+
+ while (fin < NB_COLONNES && grille[l][fin] == type) {
+ fin++;
+ }
+
+ if (fin - c >= 3) {
+ for (int k = c; k < fin; k++) {
+ aSupprimer[l][k] = true;
+ }
+ }
+
+ c = fin;
+ }
+ }
+
+ for (int col = 0; col < NB_COLONNES; col++) {
+ int l = 0;
+
+ while (l < NB_LIGNES) {
+ int type = grille[l][col];
+ int fin = l + 1;
+
+ while (fin < NB_LIGNES && grille[fin][col] == type) {
+ fin++;
+ }
+
+ if (fin - l >= 3) {
+ for (int k = l; k < fin; k++) {
+ aSupprimer[k][col] = true;
+ }
+ }
+
+ l = fin;
+ }
+ }
+
+ List positions = new ArrayList<>();
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ for (int col = 0; col < NB_COLONNES; col++) {
+ if (aSupprimer[l][col]) {
+ positions.add(new int[]{l, col});
+ }
+ }
+ }
+
+ return positions;
+ }
+
+ // -
+ // RESOLUTION AVEC CASCADE
+
+
+ public int resoudreEtRemplir() {
+ int baseTotal = 0;
+ int nbSeries = 0;
+
+ List series = trouverSeries();
+
+ while (!series.isEmpty()) {
+
+ // Accumuler les points de base et le nombre de séries sur toutes les vagues
+ baseTotal += calculerPointsBase(series);
+ nbSeries += compterNbSeries(series);
+
+ boolean[][] aSupprimer = new boolean[NB_LIGNES][NB_COLONNES];
+
+ for (int[] pos : series) {
+ aSupprimer[pos[0]][pos[1]] = true;
+ }
+
+ // Libérer les verrous des cases supprimées
+ libererVerrous(aSupprimer);
+
+ for (int col = 0; col < NB_COLONNES; col++) {
+
+ List survivants = new ArrayList<>();
+
+ for (int l = NB_LIGNES - 1; l >= 0; l--) {
+ if (!aSupprimer[l][col]) {
+ survivants.add(grille[l][col]);
+ }
+ }
+
+ int li = NB_LIGNES - 1;
+
+ for (int val : survivants) {
+ grille[li][col] = val;
+ li--;
+ }
+
+ while (li >= 0) {
+ grille[li][col] = aleatoire.nextInt(NB_TYPES);
+ li--;
+ }
+ }
+
+ series = trouverSeries();
+ }
+
+ if (nbSeries == 0) return 0;
+
+ // Bonus : +50% par série supplémentaire après la première (spec SAÉ)
+ double bonus = 1.0 + (nbSeries - 1) * 0.5;
+ return (int) (baseTotal * bonus);
+ }
+
+ private int calculerPointsBase(List series) {
+ boolean[][] masque = new boolean[NB_LIGNES][NB_COLONNES];
+
+ for (int[] pos : series) {
+ masque[pos[0]][pos[1]] = true;
+ }
+
+ int total = 0;
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ int c = 0;
+
+ while (c < NB_COLONNES) {
+ if (masque[l][c]) {
+ int fin = c + 1;
+
+ while (fin < NB_COLONNES && masque[l][fin]) {
+ fin++;
+ }
+
+ total += pointsPourLongueur(fin - c);
+ c = fin;
+ } else {
+ c++;
+ }
+ }
+ }
+
+ for (int col = 0; col < NB_COLONNES; col++) {
+ int l = 0;
+
+ while (l < NB_LIGNES) {
+ if (masque[l][col]) {
+ int fin = l + 1;
+
+ while (fin < NB_LIGNES && masque[fin][col]) {
+ fin++;
+ }
+
+ total += pointsPourLongueur(fin - l);
+ l = fin;
+ } else {
+ l++;
+ }
+ }
+ }
+
+ return total;
+ }
+
+ private int compterNbSeries(List series) {
+ boolean[][] masque = new boolean[NB_LIGNES][NB_COLONNES];
+
+ for (int[] pos : series) {
+ masque[pos[0]][pos[1]] = true;
+ }
+
+ int count = 0;
+
+ // Séries horizontales
+ for (int l = 0; l < NB_LIGNES; l++) {
+ int c = 0;
+
+ while (c < NB_COLONNES) {
+ if (masque[l][c]) {
+ int fin = c + 1;
+
+ while (fin < NB_COLONNES && masque[l][fin]) {
+ fin++;
+ }
+
+ count++;
+ c = fin;
+ } else {
+ c++;
+ }
+ }
+ }
+
+ // Séries verticales
+ for (int col = 0; col < NB_COLONNES; col++) {
+ int l = 0;
+
+ while (l < NB_LIGNES) {
+ if (masque[l][col]) {
+ int fin = l + 1;
+
+ while (fin < NB_LIGNES && masque[fin][col]) {
+ fin++;
+ }
+
+ count++;
+ l = fin;
+ } else {
+ l++;
+ }
+ }
+ }
+
+ return count;
+ }
+
+ private int pointsPourLongueur(int longueur) {
+ if (longueur == 3) {
+ return 8;
+ } else if (longueur == 4) {
+ return 16;
+ } else if (longueur == 5) {
+ return 32;
+ } else {
+ return 64;
+ }
+ }
+
+ // -
+ // VERROUS (hard mode)
+
+
+ private boolean estBloque(boolean estLigne, int index) {
+ if (estLigne) {
+ for (int col = 0; col < NB_COLONNES; col++) {
+ if (verrous[index][col]) {
+ return true;
+ }
+ }
+ } else {
+ for (int lig = 0; lig < NB_LIGNES; lig++) {
+ if (verrous[lig][index]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void ajouterVerrou() {
+ int intervalle = Math.max(1, 5 - nbCoups / 10);
+
+ if (nbCoups % intervalle != 0) {
+ return;
+ }
+
+ List casesLibres = new ArrayList<>();
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ for (int col = 0; col < NB_COLONNES; col++) {
+ if (!verrous[l][col]) {
+ casesLibres.add(new int[]{l, col});
+ }
+ }
+ }
+
+ if (casesLibres.isEmpty()) {
+ return;
+ }
+
+ int[] caseChoisie = casesLibres.get(aleatoire.nextInt(casesLibres.size()));
+ verrous[caseChoisie[0]][caseChoisie[1]] = true;
+ }
+
+ private void libererVerrous(boolean[][] aSupprimer) {
+ for (int l = 0; l < NB_LIGNES; l++) {
+ for (int col = 0; col < NB_COLONNES; col++) {
+ if (aSupprimer[l][col]) {
+ verrous[l][col] = false;
+ }
+ }
+ }
+ }
+
+ // -
+ // DETECTION FIN DE PARTIE
+
+
+ public boolean aUnCoupValide() {
+ // Teste tous les décalages possibles (1 à N-1) pour les lignes
+ for (int i = 0; i < NB_LIGNES; i++) {
+
+ // AJOUT : Si la ligne est bloquée par un verrou, on ne peut pas la bouger
+ if (estBloque(true, i)) continue;
+
+ for (int s = 1; s < NB_COLONNES; s++) {
+ if (coupCreeSerie(true, i, s)) {
+ return true;
+ }
+ }
+ }
+
+ // Teste tous les décalages possibles (1 à N-1) pour les colonnes
+ for (int j = 0; j < NB_COLONNES; j++) {
+
+ // AJOUT : Si la colonne est bloquée par un verrou, on ne peut pas la bouger
+ if (estBloque(false, j)) continue;
+
+ for (int s = 1; s < NB_LIGNES; s++) {
+ if (coupCreeSerie(false, j, s)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void forcerFinDePartie() {
+ this.partieTerminee = true;
+ }
+
+ private boolean coupCreeSerie(boolean estLigne, int index, int sens) {
+ int[][] sauvegarde = copierGrille();
+
+ if (estLigne) {
+ decalerLigne(index, sens);
+ } else {
+ decalerColonne(index, sens);
+ }
+
+ boolean resultat = !trouverSeries().isEmpty();
+
+ restaurerGrille(sauvegarde);
+
+ return resultat;
+ }
+
+ // -
+ // UTILITAIRES PRIVES
+
+
+ private int[][] copierGrille() {
+ int[][] copie = new int[NB_LIGNES][NB_COLONNES];
+
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(grille[l], 0, copie[l], 0, NB_COLONNES);
+ }
+
+ return copie;
+ }
+
+ private void restaurerGrille(int[][] sauvegarde) {
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(sauvegarde[l], 0, grille[l], 0, NB_COLONNES);
+ }
+ }
+
+
+}
diff --git a/src/src/main/java/sae/chuzzle/FinPartieActivity.java b/src/src/main/java/sae/chuzzle/FinPartieActivity.java
new file mode 100644
index 0000000..e6fe906
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/FinPartieActivity.java
@@ -0,0 +1,50 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class FinPartieActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_fin_partie);
+
+ // Récupérer les données passées par le Controleur
+ int score = getIntent().getIntExtra("score", 0);
+ int nbCoups = getIntent().getIntExtra("nbCoups", 0);
+ long graine = getIntent().getLongExtra("graine", 0L);
+
+ // Afficher les données
+ TextView tvScore = findViewById(R.id.tvFinScore);
+ TextView tvCoups = findViewById(R.id.tvFinCoups);
+ TextView tvGraine = findViewById(R.id.tvFinGraine);
+
+ tvScore.setText(String.valueOf(score));
+ tvCoups.setText(String.valueOf(nbCoups));
+ tvGraine.setText(String.valueOf(graine));
+
+ // Bouton retour au menu
+ Button btnMenu = findViewById(R.id.btnFinMenu);
+ btnMenu.setOnClickListener(v -> {
+ Intent intent = new Intent(this, MenuActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ // -
+ // RETOUR ARRIERE = menu principal
+
+ @Override
+ public void onBackPressed() {
+ Intent intent = new Intent(this, MenuActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/src/main/java/sae/chuzzle/GestionnaireTactile.java b/src/src/main/java/sae/chuzzle/GestionnaireTactile.java
new file mode 100644
index 0000000..3f9fecf
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/GestionnaireTactile.java
@@ -0,0 +1,140 @@
+package sae.chuzzle;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+public class GestionnaireTactile implements View.OnTouchListener {
+
+ private static final int NB_LIGNES = EtatJeu.NB_LIGNES;
+ private static final int NB_COLONNES = EtatJeu.NB_COLONNES;
+ 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 tailleCase;
+
+ public GestionnaireTactile(VueGrille vueGrille,
+ EtatJeu etatJeu,
+ Controleur controleur) {
+ this.vueGrille = vueGrille;
+ this.etatJeu = etatJeu;
+ this.controleur = controleur;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+
+ if (etatJeu.estTerminee()) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+
+ case MotionEvent.ACTION_DOWN:
+ tailleCase = calculerTailleCase();
+ gererDebut(event);
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ gererMouvement(event);
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ gererFin(event.getAction() == MotionEvent.ACTION_UP);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void gererDebut(MotionEvent event) {
+ touchDebutX = event.getX();
+ touchDebutY = event.getY();
+ estLigne = null;
+ decalagePx = 0f;
+
+ float margeGauche = (vueGrille.getWidth() - tailleCase * NB_COLONNES) / 2f;
+ float margeHaut = (vueGrille.getHeight() - tailleCase * NB_LIGNES) / 2f;
+
+ ligneTouchee = (int) ((touchDebutY - margeHaut) / tailleCase);
+ colonneTouchee = (int) ((touchDebutX - margeGauche) / tailleCase);
+
+ ligneTouchee = Math.max(0, Math.min(NB_LIGNES - 1, ligneTouchee));
+ colonneTouchee = Math.max(0, Math.min(NB_COLONNES - 1, colonneTouchee));
+ }
+
+ private void gererMouvement(MotionEvent event) {
+ float dx = event.getX() - touchDebutX;
+ float dy = event.getY() - touchDebutY;
+
+ if (estLigne == null) {
+ if (Math.abs(dx) > SEUIL_DIRECTION_PX || Math.abs(dy) > SEUIL_DIRECTION_PX) {
+ estLigne = Math.abs(dx) >= Math.abs(dy);
+
+ if (estLigne) {
+ indexGlissement = ligneTouchee;
+ } else {
+ indexGlissement = colonneTouchee;
+ }
+ } else {
+ return;
+ }
+ }
+
+ if (estLigne) {
+ decalagePx = dx;
+ } else {
+ decalagePx = dy;
+ }
+
+ // Affiche le glissement réel du doigt (permet la rotation multi-cases)
+ vueGrille.definirGlissement(estLigne, indexGlissement, decalagePx);
+ }
+
+ private void gererFin(boolean estRelachement) {
+
+ if (estLigne == null) {
+ vueGrille.annulerGlissement();
+ return;
+ }
+
+ if (tailleCase <= 1f) {
+ vueGrille.annulerGlissement();
+ estLigne = null;
+ return;
+ }
+
+ // Arrondi au nombre de cases le plus proche (permet ±1, ±2, ±3…)
+ int nbCases = (int) Math.round(decalagePx / tailleCase);
+
+ if (nbCases != 0 && estRelachement) {
+ boolean accepte = etatJeu.appliquerCoup(estLigne, indexGlissement, nbCases);
+ if (accepte) {
+ controleur.rafraichirAffichage();
+ controleur.verifierFinDePartie();
+ }
+ }
+
+ vueGrille.annulerGlissement();
+ estLigne = null;
+ }
+
+ private float calculerTailleCase() {
+ float l = vueGrille.getWidth();
+ float h = vueGrille.getHeight();
+
+ if (l == 0 || h == 0) {
+ return 1f;
+ }
+
+ return Math.min(l / NB_COLONNES, h / NB_LIGNES);
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/java/sae/chuzzle/MainActivity.java b/src/src/main/java/sae/chuzzle/MainActivity.java
new file mode 100644
index 0000000..1e48cd3
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/MainActivity.java
@@ -0,0 +1,116 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.RadioButton;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+public class MainActivity extends Activity implements View.OnClickListener {
+
+
+
+ private Controleur controleur;
+ private Button btnJouer;
+ private Button btnMenu;
+
+
+ private boolean hardMode;
+ private EtatJeu etatJeu;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ // --- Modèle ---
+ long graine = getIntent().getLongExtra("graine", System.currentTimeMillis());
+ hardMode = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE)
+ .getBoolean("hard_mode", false);
+ boolean daltonien = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE)
+ .getBoolean("daltonien", false);
+
+ etatJeu = new EtatJeu(graine, hardMode);
+
+ // --- Restauration si retour de pause ---
+ if (savedInstanceState != null) {
+ etatJeu.restaurerEtat(savedInstanceState);
+ }
+
+ // --- Vue ---
+ VueGrille vueGrille = findViewById(R.id.vueGrille);
+ vueGrille.definirModeDaltonien(daltonien);
+
+
+
+ // --- Controleur ---
+ btnJouer = findViewById(R.id.btnJouer);
+ btnJouer.setOnClickListener(this);
+
+ btnMenu = findViewById(R.id.btnMenu);
+ btnMenu.setOnClickListener(this);
+
+
+ controleur = new Controleur(
+ this,
+ etatJeu,
+ vueGrille,
+ graine,
+ (TextView) findViewById(R.id.tvScore),
+ (TextView) findViewById(R.id.tvCoups),
+ (RadioButton) findViewById(R.id.rbLigne),
+ (RadioButton) findViewById(R.id.rbDroite),
+ (Spinner) findViewById(R.id.spinnerIndex),
+ btnJouer,
+ hardMode
+ );
+
+ // --- Gestion tactile ---
+ GestionnaireTactile gestionnaireTactile =
+ new GestionnaireTactile(vueGrille, etatJeu, controleur);
+ vueGrille.setOnTouchListener(gestionnaireTactile);
+
+ // --- Logique Hard mode ---
+ if (hardMode) {
+ // TODO: Ajouter la nouvelle fonctionnalité pour le hard mode ici (en remplacement du timer)
+ }
+ }
+
+ // -
+ //
+
+ // -
+ // CYCLE DE VIE
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (etatJeu != null) {
+ etatJeu.sauvegarderEtat(outState);
+ }
+ if (controleur != null) {
+ controleur.sauvegarderEtat(outState);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == btnJouer) {
+ controleur.gererCoupJoueur();
+ }
+ if (v == btnMenu) {
+ etatJeu.forcerFinDePartie();
+ Intent intent = new Intent(this, MenuActivity.class);
+ startActivity(intent);
+ }
+ }
+}
diff --git a/src/src/main/java/sae/chuzzle/MenuActivity.java b/src/src/main/java/sae/chuzzle/MenuActivity.java
new file mode 100644
index 0000000..a4bd5f5
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/MenuActivity.java
@@ -0,0 +1,44 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class MenuActivity extends Activity implements View.OnClickListener {
+
+ private Button btnNouvellePartie;
+ private Button btnPartieGraine;
+ private Button btnOptions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_menu);
+
+ btnNouvellePartie = findViewById(R.id.btnNouvellePartie);
+ btnPartieGraine = findViewById(R.id.btnPartieGraine);
+ btnOptions = findViewById(R.id.btnOptions);
+
+ btnNouvellePartie.setOnClickListener(this);
+ btnPartieGraine.setOnClickListener(this);
+ btnOptions.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == btnNouvellePartie) {
+ long graine = System.currentTimeMillis();
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.putExtra("graine", graine);
+ startActivity(intent);
+ } else if (v == btnPartieGraine) {
+ Intent intent = new Intent(this, SeedActivity.class);
+ startActivity(intent);
+ } else if (v == btnOptions) {
+ Intent intent = new Intent(this, OptionsActivity.class);
+ startActivity(intent);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/java/sae/chuzzle/OptionsActivity.java b/src/src/main/java/sae/chuzzle/OptionsActivity.java
new file mode 100644
index 0000000..9d0bf7d
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/OptionsActivity.java
@@ -0,0 +1,58 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+
+public class OptionsActivity extends Activity
+ implements CompoundButton.OnCheckedChangeListener, View.OnClickListener {
+
+ private SharedPreferences prefs;
+ private CheckBox checkDaltonien;
+ private CheckBox checkHard;
+
+ private Button btnRetour;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_options);
+
+ prefs = getSharedPreferences("chuzzle_prefs", MODE_PRIVATE);
+
+ checkDaltonien = findViewById(R.id.checkDaltonien);
+ checkHard = findViewById(R.id.checkHard);
+ btnRetour = findViewById(R.id.btnBack);
+
+ checkDaltonien.setChecked(prefs.getBoolean("daltonien", false));
+ checkHard.setChecked(prefs.getBoolean("hard_mode", false));
+
+ checkDaltonien.setOnCheckedChangeListener(this);
+ checkHard.setOnCheckedChangeListener(this);
+ btnRetour.setOnClickListener(this);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton bouton, boolean estCoche) {
+ if (bouton == checkDaltonien) {
+ prefs.edit().putBoolean("daltonien", estCoche).apply();
+ } else if (bouton == checkHard) {
+ prefs.edit().putBoolean("hard_mode", estCoche).apply();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == btnRetour) {
+ Intent intent = new Intent(this, MenuActivity.class);
+ startActivity(intent);
+ }
+
+ }
+}
diff --git a/src/src/main/java/sae/chuzzle/SeedActivity.java b/src/src/main/java/sae/chuzzle/SeedActivity.java
new file mode 100644
index 0000000..50c1639
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/SeedActivity.java
@@ -0,0 +1,67 @@
+package sae.chuzzle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class SeedActivity extends Activity implements View.OnClickListener {
+
+ private EditText etGraine;
+ private Button btnJouer;
+
+ private Button btnRetour;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_seed);
+
+ etGraine = findViewById(R.id.etGraine);
+ btnJouer = findViewById(R.id.btnJouer);
+ btnRetour = findViewById(R.id.btnBack);
+
+ btnRetour.setOnClickListener(this);
+
+ btnJouer.setOnClickListener(this);
+ }
+
+ @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();
+ }
+
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.putExtra("graine", graine);
+ startActivity(intent);
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/java/sae/chuzzle/VueGrille.java b/src/src/main/java/sae/chuzzle/VueGrille.java
new file mode 100644
index 0000000..8768302
--- /dev/null
+++ b/src/src/main/java/sae/chuzzle/VueGrille.java
@@ -0,0 +1,307 @@
+package sae.chuzzle;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+public class VueGrille extends View {
+
+ // =========================================================
+ // Constantes
+ // =========================================================
+
+ private static final int NB_LIGNES = 6;
+ private static final int NB_COLONNES = 6;
+ private static final int NB_TYPES = 7;
+
+ /** Symboles pour le mode daltonien, un par type. */
+ private static final String[] SYMBOLES = {"●", "■", "▲", "✚", "★", "♦", "✿"};
+
+ // =========================================================
+ // Données métier
+ // =========================================================
+
+ /** Grille reçue depuis MainActivity (tableau brut). */
+ private int[][] grille = new int[NB_LIGNES][NB_COLONNES];
+
+ /** Verrous reçus depuis MainActivity. */
+ private boolean[][] verrous = new boolean[NB_LIGNES][NB_COLONNES];
+
+ private boolean modeDaltonien = false;
+
+ // =========================================================
+ // État de l'animation de glissement
+ // =========================================================
+
+ /**
+ * true = on anime une ligne,
+ * false = on anime une colonne,
+ * null = pas d'animation en cours.
+ */
+ private Boolean animEstLigne = null;
+
+ /** Index de la ligne ou colonne en cours de glissement. */
+ private int animIndex = 0;
+
+ /**
+ * Décalage courant en pixels (positif = droite/bas,
+ * négatif = gauche/haut).
+ */
+ private float animDecalagePx = 0f;
+
+ // =========================================================
+ // Outils de dessin
+ // =========================================================
+
+ private final Paint pinceauCase = new Paint();
+ private final Paint pinceauSymbole = new Paint();
+
+ // =========================================================
+ // Constructeurs
+ // =========================================================
+
+ public VueGrille(Context contexte) {
+ super(contexte);
+ initPinceaux();
+ }
+
+ public VueGrille(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPinceaux();
+ }
+
+ private void initPinceaux() {
+ pinceauCase.setAntiAlias(true);
+ pinceauCase.setStyle(Paint.Style.FILL);
+
+ pinceauSymbole.setAntiAlias(true);
+ pinceauSymbole.setColor(0xFF000000);
+ pinceauSymbole.setTextAlign(Paint.Align.CENTER);
+ }
+
+ // =========================================================
+ // API publique
+ // =========================================================
+
+ /**
+ * Reçoit une copie de la grille depuis MainActivity.
+ * VueGrille ne sait pas d'où viennent ces données,
+ * elle sait juste les dessiner.
+ */
+ public void definirGrille(int[][] nouvelleGrille) {
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(nouvelleGrille[l], 0, grille[l], 0, NB_COLONNES);
+ }
+ invalidate();
+ }
+
+ public void definirModeDaltonien(boolean actif) {
+ this.modeDaltonien = actif;
+ invalidate();
+ }
+
+ public void definirVerrous(boolean[][] nouveauxVerrous) {
+ for (int l = 0; l < NB_LIGNES; l++) {
+ System.arraycopy(nouveauxVerrous[l], 0, verrous[l], 0, NB_COLONNES);
+ }
+ invalidate();
+ }
+
+ // =========================================================
+ // API animation de glissement (appelée par GestionnaireTactile)
+ // =========================================================
+
+ /**
+ * Déclenche l'animation de glissement en temps réel.
+ *
+ * @param estLigne true = on déplace une ligne, false = une colonne
+ * @param index index de la ligne ou colonne (0-based)
+ * @param decalagePx décalage courant en pixels (peut être négatif)
+ */
+ public void definirGlissement(boolean estLigne, int index, float decalagePx) {
+ this.animEstLigne = estLigne;
+ this.animIndex = index;
+ this.animDecalagePx = decalagePx;
+ invalidate();
+ }
+
+ /**
+ * Annule l'animation de glissement et revient à l'affichage normal.
+ */
+ public void annulerGlissement() {
+ this.animEstLigne = null;
+ invalidate();
+ }
+
+ // =========================================================
+ // Dessin
+ // =========================================================
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+
+ int largeur = getWidth();
+ int hauteur = getHeight();
+
+ float tailleCase = Math.min(
+ largeur / (float) NB_COLONNES,
+ hauteur / (float) NB_LIGNES
+ );
+
+ float margeGauche = (largeur - tailleCase * NB_COLONNES) / 2f;
+ float margeHaut = (hauteur - tailleCase * NB_LIGNES) / 2f;
+
+ pinceauSymbole.setTextSize(tailleCase * 0.4f);
+
+ for (int ligne = 0; ligne < NB_LIGNES; ligne++) {
+ for (int colonne = 0; colonne < NB_COLONNES; colonne++) {
+
+ // --------------------------------------------------
+ // Calcul de l'offset d'animation pour cette case
+ // --------------------------------------------------
+ float offsetX = 0f;
+ float offsetY = 0f;
+
+ if (animEstLigne != null) {
+ if (animEstLigne && ligne == animIndex) {
+ // Décalage horizontal de toute la ligne
+ offsetX = animDecalagePx;
+ } else if (!animEstLigne && colonne == animIndex) {
+ // Décalage vertical de toute la colonne
+ offsetY = animDecalagePx;
+ }
+ }
+
+ // --------------------------------------------------
+ // Position de base de la case
+ // --------------------------------------------------
+ float x1 = margeGauche + colonne * tailleCase + 6 + offsetX;
+ float y1 = margeHaut + ligne * tailleCase + 6 + offsetY;
+ float x2 = margeGauche + (colonne + 1) * tailleCase - 6 + offsetX;
+ float y2 = margeHaut + (ligne + 1) * tailleCase - 6 + offsetY;
+
+ // --------------------------------------------------
+ // Dessin avec wrap-around (la case qui sort d'un
+ // côté réapparaît de l'autre côté)
+ // --------------------------------------------------
+ dessinerCase(canvas, ligne, colonne, x1, y1, x2, y2,
+ tailleCase, margeGauche, margeHaut, offsetX, offsetY);
+ }
+ }
+ }
+
+ /**
+ * Dessine une case à la position donnée.
+ * Si la case sort des bords (wrap-around), elle est aussi dessinée
+ * du côté opposé de la grille.
+ */
+ private void dessinerCase(Canvas canvas,
+ int ligne, int colonne,
+ float x1, float y1, float x2, float y2,
+ float tailleCase,
+ float margeGauche, float margeHaut,
+ float offsetX, float offsetY) {
+
+ int type = grille[ligne][colonne];
+
+ // Bornage de la grille (en pixels)
+ float borneGaucheGrille = margeGauche;
+ float bordDroiteGrille = margeGauche + NB_COLONNES * tailleCase;
+ float borneHautGrille = margeHaut;
+ float bordBasGrille = margeHaut + NB_LIGNES * tailleCase;
+
+ // Largeur totale de la ligne / hauteur totale de la colonne en pixels
+ float largeurGrille = NB_COLONNES * tailleCase;
+ float hauteurGrille = NB_LIGNES * tailleCase;
+
+ // Dessine la case principale
+ dessinerRectCase(canvas, type, ligne, colonne, x1, y1, x2, y2);
+
+ // Wrap-around horizontal (pour les lignes)
+ if (offsetX != 0f) {
+ float wrapX1 = x1, wrapX2 = x2;
+
+ if (x2 > bordDroiteGrille) {
+ // La case déborde à droite → réapparaît à gauche
+ wrapX1 = x1 - largeurGrille;
+ wrapX2 = x2 - largeurGrille;
+ dessinerRectCase(canvas, type, ligne, colonne, wrapX1, y1, wrapX2, y2);
+ } else if (x1 < borneGaucheGrille) {
+ // La case déborde à gauche → réapparaît à droite
+ wrapX1 = x1 + largeurGrille;
+ wrapX2 = x2 + largeurGrille;
+ dessinerRectCase(canvas, type, ligne, colonne, wrapX1, y1, wrapX2, y2);
+ }
+ }
+
+ // Wrap-around vertical (pour les colonnes)
+ if (offsetY != 0f) {
+ float wrapY1 = y1, wrapY2 = y2;
+
+ if (y2 > bordBasGrille) {
+ // La case déborde en bas → réapparaît en haut
+ wrapY1 = y1 - hauteurGrille;
+ wrapY2 = y2 - hauteurGrille;
+ dessinerRectCase(canvas, type, ligne, colonne, x1, wrapY1, x2, wrapY2);
+ } else if (y1 < borneHautGrille) {
+ // La case déborde en haut → réapparaît en bas
+ wrapY1 = y1 + hauteurGrille;
+ wrapY2 = y2 + hauteurGrille;
+ dessinerRectCase(canvas, type, ligne, colonne, x1, wrapY1, x2, wrapY2);
+ }
+ }
+ }
+
+ /**
+ * Dessine le rectangle coloré d'une case + verrou + symbole daltonien.
+ */
+ private void dessinerRectCase(Canvas canvas, int type,
+ int ligne, int colonne,
+ float x1, float y1, float x2, float y2) {
+
+ // Couleur de fond
+ definirCouleur(type);
+ canvas.drawRoundRect(x1, y1, x2, y2, 20, 20, pinceauCase);
+
+ // Assombrir si verrouillée
+ if (verrous[ligne][colonne]) {
+ pinceauCase.setARGB(120, 0, 0, 0);
+ canvas.drawRoundRect(x1, y1, x2, y2, 20, 20, pinceauCase);
+ }
+
+ float cx = (x1 + x2) / 2f;
+ float cy = (y1 + y2) / 2f
+ - (pinceauSymbole.descent() + pinceauSymbole.ascent()) / 2f;
+
+ // Symbole daltonien
+ if (modeDaltonien) {
+ canvas.drawText(SYMBOLES[type % NB_TYPES], cx, cy, pinceauSymbole);
+ }
+
+ // Cadenas par-dessus si verrouillée
+ if (verrous[ligne][colonne]) {
+ canvas.drawText("🔒", cx, cy, pinceauSymbole);
+ }
+ }
+
+ // =========================================================
+ // Utilitaire privé
+ // =========================================================
+
+ private void definirCouleur(int type) {
+ switch (type % NB_TYPES) {
+ case 0: pinceauCase.setARGB(255, 200, 200, 200); break;
+ case 1: pinceauCase.setARGB(255, 255, 105, 180); break;
+ case 2: pinceauCase.setARGB(255, 90, 230, 200); break;
+ case 3: pinceauCase.setARGB(255, 100, 170, 255); break;
+ case 4: pinceauCase.setARGB(255, 255, 220, 90); break;
+ case 5: pinceauCase.setARGB(255, 255, 140, 90); break;
+ case 6: pinceauCase.setARGB(255, 255, 90, 90); break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/src/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/drawable/ic_launcher_background.xml b/src/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/src/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/main/res/drawable/ic_launcher_foreground.xml b/src/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/src/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/layout/activity_fin_partie.xml b/src/src/main/res/layout/activity_fin_partie.xml
new file mode 100644
index 0000000..bca6399
--- /dev/null
+++ b/src/src/main/res/layout/activity_fin_partie.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/main/res/layout/activity_main.xml b/src/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..642f8e8
--- /dev/null
+++ b/src/src/main/res/layout/activity_main.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/layout/activity_menu.xml b/src/src/main/res/layout/activity_menu.xml
new file mode 100644
index 0000000..1c1f1e5
--- /dev/null
+++ b/src/src/main/res/layout/activity_menu.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/layout/activity_options.xml b/src/src/main/res/layout/activity_options.xml
new file mode 100644
index 0000000..92a86aa
--- /dev/null
+++ b/src/src/main/res/layout/activity_options.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/main/res/layout/activity_seed.xml b/src/src/main/res/layout/activity_seed.xml
new file mode 100644
index 0000000..7ac4c4a
--- /dev/null
+++ b/src/src/main/res/layout/activity_seed.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/src/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/src/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/mipmap-hdpi/ic_launcher.webp b/src/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/src/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/src/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/src/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/src/src/main/res/mipmap-mdpi/ic_launcher.webp b/src/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/src/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/src/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/src/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/src/src/main/res/mipmap-xhdpi/ic_launcher.webp b/src/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/src/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/src/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/src/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/src/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/src/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/src/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/src/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/src/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/src/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/src/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/src/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/src/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/src/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/src/src/main/res/values-night/themes.xml b/src/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..af00a57
--- /dev/null
+++ b/src/src/main/res/values-night/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/values/colors.xml b/src/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/src/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/src/src/main/res/values/strings.xml b/src/src/main/res/values/strings.xml
new file mode 100644
index 0000000..30bf2ee
--- /dev/null
+++ b/src/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ RealChuzzle
+
\ No newline at end of file
diff --git a/src/src/main/res/values/themes.xml b/src/src/main/res/values/themes.xml
new file mode 100644
index 0000000..af00a57
--- /dev/null
+++ b/src/src/main/res/values/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/xml/backup_rules.xml b/src/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/src/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/xml/data_extraction_rules.xml b/src/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/src/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/src/main/res/xml/preferences.xml b/src/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..55418ef
--- /dev/null
+++ b/src/src/main/res/xml/preferences.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/src/test/java/sae/chuzzle/ExampleUnitTest.java b/src/src/test/java/sae/chuzzle/ExampleUnitTest.java
new file mode 100644
index 0000000..72be210
--- /dev/null
+++ b/src/src/test/java/sae/chuzzle/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package sae.chuzzle;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file