POUR LE BINÔME ADULTE
Lignes - : retirées. Lignes + : ajoutées. Ce fichier ne se lance pas.

--- rubiks_etape_09.py
+++ rubiks_etape_10.py
@@ -1,5 +1,5 @@
-"""Rubik's Cube — étape 09 : la 3D avec Pygame seulement.
-Lancement : py rubiks_etape_09.py
+"""Rubik's Cube — étape 10 : la 3D avec Pygame seulement.
+Lancement : py rubiks_etape_10.py
 Fichier autonome. Les étapes 01 à 08 restent inchangées.
 """
 
@@ -226,10 +226,80 @@
     fenetre.set_clip(ancien_cadrage)
 
 
+class Partie:
+    """La mémoire et l'animation restent séparées du dessin de la fenêtre."""
+    def __init__(self):
+        self.reinitialiser()
+
+    def reinitialiser(self):
+        self.cube = creer_cube()
+        self.file = deque()
+        self.animation = None
+        self.historique = []
+        self.compteur = 0
+        self.message = "À toi de jouer !"
+
+    def commander(self, action):
+        if action == "reset":
+            self.reinitialiser()  # Annule aussi une animation ou des coups en attente.
+        elif action == "melanger":
+            self.reinitialiser()
+            precedente = None
+            for coup in range(LONGUEUR_MELANGE):
+                face = random.choice([f for f in COULEURS if f != precedente])
+                self.file.append((face, random.choice([False, True]), "melange"))
+                precedente = face
+            self.message = "Mélange animé en cours..."
+        elif len(self.file) < 64:
+            if action == "annuler":
+                self.file.append("annuler")
+            else:
+                face, inverse = action
+                self.file.append((face, inverse, "joueur"))
+        else:
+            self.message = "Attends un peu : la file de coups est pleine."
+
+    def avancer(self, secondes):
+        if self.animation is None:
+            while self.file:
+                commande = self.file.popleft()
+                if commande == "annuler":
+                    if not self.historique:
+                        self.message = "Aucun coup à annuler."
+                        continue
+                    face, inverse = self.historique[-1]
+                    inverse = not inverse
+                    genre = "annulation"
+                else:
+                    face, inverse, genre = commande
+                self.animation = {"face": face, "inverse": inverse, "genre": genre,
+                                  "temps": 0.0, "duree": max(0.05, DUREE_TOUR)
+                                  if genre != "melange" else 0.12}
+                break
+        if self.animation is None:
+            return
+        a = self.animation
+        a["temps"] += max(0, secondes)
+        if a["temps"] >= a["duree"]:
+            # La mémoire change à la fin : aucun saut au milieu de l'animation.
+            tourner(self.cube, a["face"], a["inverse"])
+            if a["genre"] == "joueur":
+                self.historique.append((a["face"], a["inverse"]))
+                self.compteur += 1
+                self.message = "Dernier coup : " + a["face"] + ("'" if a["inverse"] else "")
+            elif a["genre"] == "annulation":
+                self.historique.pop()
+                self.compteur -= 1
+                self.message = "Dernier coup annulé."
+            elif not self.file:
+                self.message = "Mélange prêt. À toi de jouer !"
+            self.animation = None
+
+
 def main():
     pygame.init()
     fenetre = pygame.display.set_mode((1040, 740))
-    pygame.display.set_caption("Mon Rubik's Cube 3D - Etape 09")
+    pygame.display.set_caption("Mon Rubik's Cube 3D - Etape 10")
     horloge = pygame.time.Clock()
     police = pygame.font.Font(None, 26)
     petite = pygame.font.Font(None, 22)
@@ -237,8 +307,11 @@
     camera = Camera()
     glisser = False
     lettres = False
-    cube = creer_cube()
+    partie = Partie()
     boutons = []
+    for indice, face in enumerate("UDFBLR"):
+        boutons.append((pygame.Rect(725, 125 + indice*45, 115, 35), face, (face, False)))
+        boutons.append((pygame.Rect(855, 125 + indice*45, 115, 35), face + "'", (face, True)))
     boutons.append((pygame.Rect(725, 553, 245, 36), "Recentrer la vue", "camera"))
     boutons.append((pygame.Rect(725, 599, 245, 36), "Afficher les lettres", "lettres"))
     en_marche = True
@@ -271,14 +344,20 @@
                     action = "camera"
                 elif evenement.key == pygame.K_TAB:
                     action = "lettres"
+                elif pygame.key.name(evenement.key).upper() in COULEURS:
+                    action = (pygame.key.name(evenement.key).upper(),
+                              bool(evenement.mod & pygame.KMOD_SHIFT))
             if action == "camera":
                 camera.recentrer()
             elif action == "lettres":
                 lettres = not lettres
+            elif action is not None:
+                partie.commander(action)
+        partie.avancer(secondes)
         fenetre.fill(FOND)
         fenetre.blit(titre.render(TITRE, True, (245, 245, 250)), (30, 25))
         pygame.draw.ellipse(fenetre, (15, 19, 29), (155, 555, 370, 42))
-        dessiner_cube(fenetre, cube, camera, petite, lettres)
+        dessiner_cube(fenetre, partie.cube, camera, petite, lettres, partie.animation)
         pygame.draw.rect(fenetre, (31, 38, 55), (700, 85, 300, 620), border_radius=16)
         fenetre.blit(police.render("Les commandes", True, (245, 245, 250)), (725, 95))
         for rectangle, texte, commande in boutons:
@@ -291,10 +370,19 @@
                  "Début : recentrer | Tab : lettres",
                  "U haut | D bas | F devant",
                  "B derrière | L gauche | R droite"]
+        aides += ["U D F B L R : tourner une face", "Maj + lettre : sens inverse"]
         for indice, texte in enumerate(aides):
             colonne, ligne = divmod(indice, 5)
             fenetre.blit(petite.render(texte, True, (210, 219, 238)),
                          (30 + colonne*345, 610 + ligne*22))
+        if partie.animation:
+            nom = partie.animation["face"] + ("'" if partie.animation["inverse"] else "")
+            etat = "Rotation " + nom + "..."
+        elif partie.file:
+            etat = "Mouvements en attente"
+        else:
+            etat = "Cube résolu !" if est_resolu(partie.cube) else "Cube en cours"
+        fenetre.blit(petite.render(etat, True, (255, 222, 110)), (725, 654))
         pygame.display.flip()
     pygame.quit()
 
