Développer un jeu de tri de liquides peut sembler simple… jusqu’à ce qu’on décide de le faire proprement, avec une interface claire, une logique interne robuste, une grille toujours solvable et une expérience utilisateur fluide.
Ce projet Magic Liquid Sort démontre comment une architecture bien pensée permet de créer un jeu stratégique, agréable et parfaitement fonctionnel — même sans animation.
Une interface graphique construite au pixel près
Le programme définit chaque élément visuel avec précision :
hautTube vaut hautCol + capacite * hautDose + 8 rectangle_arrondi(ecran, sX, sY, largTube, hautTube, 25, #e8f1ff)
Les tubes, les doses, les reflets, les couleurs… tout est dessiné manuellement.
La palette est soigneusement pensée :
- teintes : couleurs principales des liquides,
- teintesClaires : nuances pour les reflets,
- libelles : noms des couleurs pour les messages.
Le résultat : une interface propre, moderne, lisible, qui fonctionne sans animation tout en restant agréable à l’œil.
Une logique de jeu solide et entièrement contrôlée
Le cœur du jeu repose sur deux structures essentielles :
- contenu : tableau plat représentant chaque dose dans chaque tube,
- niveaux : nombre de doses présentes dans chaque tube.
Le programme garantit que chaque grille générée est résolvable, grâce à une technique intelligente :
- Mélange par COUPS INVERSES : la grille obtenue est donc toujours resolvable.
Autrement dit : le mélange est effectué uniquement via des mouvements qui pourraient être annulés.
Une gestion des interactions simple, intuitive et efficace Le système gère :
- la sélection d’un tube,
- la validation des règles de versement,
- les incompatibilités de couleurs,
- les tubes pleins ou vides,
- l’annulation des coups via un historique,
- la création d’une nouvelle grille.
Chaque action est accompagnée d’un message clair :
- Ce tube est vide : rien à prendre.
- Le tube est plein.
- Coup annulé.
Même sans animation, l’expérience utilisateur reste fluide, compréhensible et agréable.
Un tri logique fidèle aux règles du genre
Le programme applique strictement les règles du tri :
- On ne verse que sur une couleur identique ou dans un tube vide.
- On ne dépasse jamais la capacité.
- On verse plusieurs doses d’un coup si elles sont consécutives et identiques.
- On ne casse jamais une séquence homogène.
Le code le montre clairement :
- si contenu[cible * capacite + nDst - 1] = doseHaut alors okVerser vaut vrai
Une logique propre, lisible, efficace.
Détection automatique de la victoire
La fonction de vérification est simple et rigoureuse :
- si contenu[...] <> gRef alors retourne faux
Le jeu est gagné uniquement si :
- chaque tube est rempli,
- chaque tube contient une seule couleur,
- aucune dose n’est mal placée.
Une bannière de victoire apparaît alors, accompagnée du nombre de coups réalisés.
Ce que ce projet met en valeur
- Conception d’une interface graphique complète sans animation
- Gestion avancée de l’état du jeu
- Génération de grilles toujours solvables
- Système d’annulation avec historique
- UX claire, messages guidants, boutons interactifs
- Architecture modulaire et extensible
- Logique de tri fidèle aux jeux du genre
Programme :
// ============================================================
// 1. REGLAGES DU JEU
// ============================================================
nbTubes est un nombre
nbTubes vaut 8
nbCol est un nombre
nbCol vaut 4
capacite est un nombre
capacite vaut 4
nbCouleurs est un nombre
nbCouleurs vaut 6
// ---- Geometrie ----
largTube est un nombre
largTube vaut 62
hautDose est un nombre
hautDose vaut 32
hautCol est un nombre
hautCol vaut 16
hautTube est un nombre
hautTube vaut hautCol + capacite * hautDose + 8
espaceX est un nombre
espaceX vaut 26
espaceY est un nombre
espaceY vaut 48
margeH est un nombre
margeH vaut 26
hautEntete est un nombre
hautEntete vaut 62
margeB est un nombre
margeB vaut 112
largToile est un nombre
largToile vaut margeH * 2 + nbCol * largTube + (nbCol - 1) * espaceX
hautToile est un nombre
hautToile vaut hautEntete + 2 * hautTube + espaceY + margeB
// ---- Boutons ----
btnL est un nombre
btnL vaut 148
btnH est un nombre
btnH vaut 42
btnY est un nombre
btnY vaut hautToile - 58
btnX1 est un nombre
btnX1 vaut margeH
btnX2 est un nombre
btnX2 vaut largToile - margeH - btnL
// ============================================================
// 2. PALETTE (indice 0 = vide)
// ============================================================
teintes est un tableau
teintes ajoute #14243c
teintes ajoute #ff3d8b
teintes ajoute #a855f7
teintes ajoute #3b82f6
teintes ajoute #22d3ee
teintes ajoute #4ade80
teintes ajoute #fbbf24
teintesClaires est un tableau
teintesClaires ajoute #14243c
teintesClaires ajoute #ff85b5
teintesClaires ajoute #c99bff
teintesClaires ajoute #93bcff
teintesClaires ajoute #8beff9
teintesClaires ajoute #a7f3c4
teintesClaires ajoute #ffd96b
libelles est un tableau
libelles ajoute "vide"
libelles ajoute "rose"
libelles ajoute "violet"
libelles ajoute "bleu"
libelles ajoute "cyan"
libelles ajoute "vert"
libelles ajoute "jaune"
// ============================================================
// 3. ETAT DU JEU
// contenu : tableau PLAT de nbTubes * capacite cases
// case (tube * capacite + dose) = numero de couleur (0 = vide)
// niveaux : nombre de doses presentes dans chaque tube
// ============================================================
contenu est un tableau
niveaux est un tableau
jeu est une toile
selection est un nombre
selection vaut - 1
coups est un nombre
coups vaut 0
gagne est un booleen
gagne vaut faux
message est un texte
message vaut "Clique sur un tube pour commencer."
histoSrc est un tableau
histoDst est un tableau
histoNb est un tableau
// ============================================================
// 4. FONCTIONS DE POSITION
// ============================================================
fonction posX(idxX)
pCol est un nombre
pCol vaut idxX mod nbCol
retourne margeH + pCol * (largTube + espaceX)
fin fonction
fonction posY(idxY)
pRang est un nombre
pRang vaut arrondi_inferieur(idxY / nbCol)
retourne hautEntete + pRang * (hautTube + espaceY)
fin fonction
// ============================================================
// 5. LE JEU EST-IL GAGNE ?
// ============================================================
fonction estGagne()
gk est un nombre
gd est un nombre
gn est un nombre
gRef est un nombre
pour gk de 0 à nbTubes - 1
gn vaut niveaux[gk]
si gn > 0 alors
si gn <> capacite alors
retourne faux
fin si
gRef vaut contenu[gk * capacite]
pour gd de 1 à gn - 1
si contenu[gk * capacite + gd] <> gRef alors
retourne faux
fin si
fin pour
fin si
fin pour
retourne vrai
fin fonction
// ============================================================
// 6. DESSIN COMPLET SUR UNE TOILE
// tubeSel : tube selectionne (-1 si aucun)
// ============================================================
procédure dessinerSurToile(ecran, tubeSel, avecVictoire)
dimension(ecran, largToile, hautToile)
effacer(ecran)
dégradé(ecran, [#080f1d, #17243f, #080f1d])
sTxt est un texte
sIdx est un nombre
sD est un nombre
sX est un nombre
sY est un nombre
sBase est un nombre
sNb est un nombre
sCoul est un nombre
sYd est un nombre
sVisible est un booleen
// ---- Entete ----
label(ecran, margeH, 20, "MAGIC LIQUID SORT", #eaf2ff, 15)
sTxt vaut "Coups : " + convertir_texte(coups)
label(ecran, largToile - margeH - label_largeur(sTxt, 14), 20, sTxt, #8fb3e0, 14)
// ---- Les tubes ----
pour sIdx de 0 à nbTubes - 1
sX vaut posX(sIdx)
sY vaut posY(sIdx)
sBase vaut sY + hautTube - 5
sNb vaut niveaux[sIdx]
// Coque exterieure (blanche, ou doree si le tube est selectionne)
si sIdx = tubeSel alors
rectangle_arrondi(ecran, sX, sY, largTube, hautTube, 25, #ffd166)
sinon
rectangle_arrondi(ecran, sX, sY, largTube, hautTube, 25, #e8f1ff)
fin si
// Interieur du verre
rectangle_arrondi(ecran, sX + 3, sY + 3, largTube - 6, hautTube - 6, 22, #14243c)
// Les doses de liquide (de bas en haut)
pour sD de 0 à sNb - 1
sCoul vaut contenu[sIdx * capacite + sD]
sYd vaut sBase - (sD + 1) * hautDose
// la dose du haut du tube selectionne est dessinee plus loin (elle "flotte")
sVisible vaut vrai
si sIdx = tubeSel et sD = sNb - 1 alors
sVisible vaut faux
fin si
si sVisible alors
si sD = 0 alors
// fond arrondi comme un vrai tube a essai
rectangle_arrondi(ecran, sX + 5, sYd, largTube - 10, hautDose, 16, teintes[sCoul])
rectangle(ecran, sX + 5, sYd, largTube - 10, hautDose - 16, teintes[sCoul])
sinon
rectangle(ecran, sX + 5, sYd, largTube - 10, hautDose, teintes[sCoul])
fin si
// surface plus claire sur la dose du dessus
si sD = sNb - 1 alors
rectangle(ecran, sX + 5, sYd, largTube - 10, 4, teintesClaires[sCoul])
fin si
fin si
fin pour
// Reflet du verre
rectangle_arrondi(ecran, sX + 12, sY + 30, 5, 46, 3, #cfe0f5)
// Col du tube
rectangle_arrondi(ecran, sX + 10, sY + 9, largTube - 20, 7, 3, #0b1526)
// Dose qui flotte au-dessus du tube selectionne
si sIdx = tubeSel et sNb > 0 alors
sCoul vaut contenu[sIdx * capacite + sNb - 1]
rectangle_arrondi(ecran, sX + 5, sY - 28, largTube - 10, hautDose, 12, teintes[sCoul])
rectangle(ecran, sX + 5, sY - 24, largTube - 10, 4, teintesClaires[sCoul])
fin si
// Numero du tube
sTxt vaut convertir_texte(sIdx + 1)
label(ecran, sX + largTube / 2 - label_largeur(sTxt, 14) / 2, sY + hautTube + 20, sTxt, #6f8cb8, 14)
fin pour
// ---- Message ----
label(ecran, margeH, btnY - 14, message, #9fb8dd, 13)
// ---- Boutons ----
rectangle_arrondi(ecran, btnX1, btnY, btnL, btnH, 12, #1d3557)
sTxt vaut "Annuler"
label(ecran, btnX1 + btnL / 2 - label_largeur(sTxt, 15) / 2, btnY + 27, sTxt, #cfe3ff, 15)
rectangle_arrondi(ecran, btnX2, btnY, btnL, btnH, 12, #1d3557)
sTxt vaut "Nouveau"
label(ecran, btnX2 + btnL / 2 - label_largeur(sTxt, 15) / 2, btnY + 27, sTxt, #cfe3ff, 15)
// ---- Banniere de victoire ----
si avecVictoire alors
rectangle_arrondi(ecran, margeH, hautToile / 2 - 46, largToile - margeH * 2, 92, 18, #0e2c47)
sTxt vaut "GAGNE !"
label(ecran, largToile / 2 - label_largeur(sTxt, 26) / 2, hautToile / 2 - 6, sTxt, #ffd166, 26)
sTxt vaut "Tubes tries en " + convertir_texte(coups) + " coups"
label(ecran, largToile / 2 - label_largeur(sTxt, 14) / 2, hautToile / 2 + 24, sTxt, #cfe3ff, 14)
fin si
fin procédure
// ============================================================
// 7. AFFICHAGE DE L'ETAT COURANT
// ============================================================
procédure dessiner(selVue, gagneVue)
appelle dessinerSurToile(jeu, selVue, gagneVue)
affiche jeu
fin procédure
// ============================================================
// 8. VARIABLES DE LA BOUCLE PRINCIPALE
// ============================================================
partieEnCours est un booleen
nouvelleDemande est un booleen
rejouer est un nombre
idx est un nombre
dd est un nombre
essai est un nombre
tD est un nombre
tS est un nombre
nD est un nombre
couleurBouge est un nombre
okBouge est un booleen
clicX est un nombre
clicY est un nombre
cible est un nombre
nSrc est un nombre
nDst est un nombre
doseHaut est un nombre
okVerser est un booleen
nbVerse est un nombre
placeLibre est un nombre
poursuivre est un booleen
dernierIdx est un nombre
uSrc est un nombre
uDst est un nombre
uNb est un nombre
coulRetour est un nombre
// ============================================================
// 9. PARTIE
// ============================================================
partieEnCours vaut vrai
tant que partieEnCours
// ---------- Distribution : tubes tries ----------
vide contenu
vide niveaux
vide histoSrc
vide histoDst
vide histoNb
pour idx de 0 à nbTubes - 1
niveaux ajoute 0
pour dd de 0 à capacite - 1
contenu ajoute 0
fin pour
fin pour
pour idx de 0 à nbCouleurs - 1
niveaux[idx] vaut capacite
pour dd de 0 à capacite - 1
contenu[idx * capacite + dd] vaut idx + 1
fin pour
fin pour
// ---------- Melange par COUPS INVERSES ----------
// (on ne fait que des coups qui peuvent etre annules :
// la grille obtenue est donc toujours resolvable)
tant que estGagne()
pour essai de 1 à 500
tD vaut hasard(0, nbTubes - 1)
tS vaut hasard(0, nbTubes - 1)
si tD <> tS alors
nD vaut niveaux[tD]
si nD > 0 alors
si niveaux[tS] < capacite alors
couleurBouge vaut contenu[tD * capacite + nD - 1]
okBouge vaut faux
si nD = 1 alors
okBouge vaut vrai
sinon si contenu[tD * capacite + nD - 2] = couleurBouge alors
okBouge vaut vrai
fin si
si okBouge alors
contenu[tD * capacite + nD - 1] vaut 0
niveaux[tD] vaut nD - 1
contenu[tS * capacite + niveaux[tS]] vaut couleurBouge
niveaux[tS] vaut niveaux[tS] + 1
fin si
fin si
fin si
fin si
fin pour
fin tant que
coups vaut 0
selection vaut - 1
gagne vaut faux
nouvelleDemande vaut faux
message vaut "Clique sur un tube pour commencer."
// ---------- Boucle de jeu ----------
tant que non gagne et non nouvelleDemande
appelle dessiner(selection, faux)
cliquer jeu dans clicX, clicY
// Sur quel tube a-t-on clique ?
cible vaut - 1
pour idx de 0 à nbTubes - 1
si clique(clicX, clicY, posX(idx) - 4, posY(idx) - 28, largTube + 8, hautTube + 36) alors
cible vaut idx
fin si
fin pour
si clique(clicX, clicY, btnX1, btnY, btnL, btnH) alors
// ---- BOUTON ANNULER ----
si longueur(histoSrc) = 0 alors
message vaut "Rien a annuler."
sinon
dernierIdx vaut longueur(histoSrc) - 1
uSrc vaut histoSrc[dernierIdx]
uDst vaut histoDst[dernierIdx]
uNb vaut histoNb[dernierIdx]
pour dd de 1 à uNb
niveaux[uDst] vaut niveaux[uDst] - 1
coulRetour vaut contenu[uDst * capacite + niveaux[uDst]]
contenu[uDst * capacite + niveaux[uDst]] vaut 0
contenu[uSrc * capacite + niveaux[uSrc]] vaut coulRetour
niveaux[uSrc] vaut niveaux[uSrc] + 1
fin pour
histoSrc supprime dernierIdx
histoDst supprime dernierIdx
histoNb supprime dernierIdx
coups vaut coups + 1
selection vaut - 1
message vaut "Coup annule."
fin si
sinon si clique(clicX, clicY, btnX2, btnY, btnL, btnH) alors
// ---- BOUTON NOUVEAU ----
nouvelleDemande vaut vrai
affiche ">> Nouvelle grille."
sinon si cible = -1 alors
selection vaut - 1
message vaut "Clique sur un tube."
sinon si selection = -1 alors
// ---- PREMIER CLIC : on prend ----
si niveaux[cible] = 0 alors
message vaut "Ce tube est vide : rien a prendre."
sinon
selection vaut cible
message vaut "Tube " + convertir_texte(cible + 1) + " pris. Ou le verser ?"
fin si
sinon si selection = cible alors
// ---- MEME TUBE : on repose ----
selection vaut - 1
message vaut "Selection annulee."
sinon
// ---- DEUXIEME CLIC : on verse ----
nSrc vaut niveaux[selection]
nDst vaut niveaux[cible]
doseHaut vaut contenu[selection * capacite + nSrc - 1]
okVerser vaut faux
si nDst >= capacite alors
message vaut "Le tube " + convertir_texte(cible + 1) + " est plein."
sinon
si nDst = 0 alors
okVerser vaut vrai
sinon si contenu[cible * capacite + nDst - 1] = doseHaut alors
okVerser vaut vrai
sinon
message vaut "Le " + libelles[doseHaut] + " ne va pas sur le " + libelles[contenu[cible * capacite + nDst - 1]] + "."
fin si
fin si
si okVerser alors
// combien de doses identiques a la suite ?
nbVerse vaut 1
dd vaut nSrc - 2
poursuivre vaut vrai
tant que poursuivre
si dd < 0 alors
poursuivre vaut faux
sinon si contenu[selection * capacite + dd] <> doseHaut alors
poursuivre vaut faux
sinon
nbVerse vaut nbVerse + 1
dd vaut dd - 1
fin si
fin tant que
placeLibre vaut capacite - nDst
si nbVerse > placeLibre alors
nbVerse vaut placeLibre
fin si
// on retire du tube de depart
pour dd de 1 à nbVerse
contenu[selection * capacite + niveaux[selection] - 1] vaut 0
niveaux[selection] vaut niveaux[selection] - 1
fin pour
// on ajoute dans le tube d'arrivee
pour dd de 1 à nbVerse
contenu[cible * capacite + niveaux[cible]] vaut doseHaut
niveaux[cible] vaut niveaux[cible] + 1
fin pour
histoSrc ajoute selection
histoDst ajoute cible
histoNb ajoute nbVerse
coups vaut coups + 1
message vaut convertir_texte(nbVerse) + " dose(s) de " + libelles[doseHaut] + " versee(s) dans le tube " + convertir_texte(cible + 1) + "."
fin si
selection vaut - 1
fin si
si estGagne() alors
gagne vaut vrai
fin si
fin tant que
// ---------- Fin de partie ----------
si gagne alors
message vaut "Bravo, tous les tubes sont tries !"
appelle dessiner(-1, vrai)
affiche "========================================"
affiche "GAGNE ! Tubes tries en " + convertir_texte(coups) + " coups."
rejouer vaut 0
choisir ["Rejouer une partie", "Quitter"] dans rejouer
si rejouer = 2 alors
partieEnCours vaut faux
fin si
fin si
fin tant que





