POUR LE BINÔME ADULTE
Lignes - : retirées. Lignes + : ajoutées. Ce fichier ne se lance pas.
Les étapes 12 à 14 partagent atelier_cube_reel.py, resolution_cube.py,
rubiks_etape_11.py et le dossier rubik fournis dans etapes.
Les nouveaux fichiers communs sont des sources complètes à conserver ensemble.

--- rubiks_etape_11.py
+++ rubiks_etape_12.py
@@ -1,405 +1,11 @@
-"""Rubik's Cube — étape 11 : la 3D avec Pygame seulement.
-Lancement : py rubiks_etape_11.py
-Fichier autonome. Les étapes 01 à 08 restent inchangées.
+"""Étape 12 — Recopier les couleurs du vrai cube.
+
+Lance ce fichier depuis le dossier complet : il utilise atelier_cube_reel.py,
+resolution_cube.py, rubiks_etape_11.py et le dossier rubik fournis à côté.
+Installation : py -m pip install pygame
+Lancement : py rubiks_etape_12.py
 """
-
-import math
-import random
-from collections import deque
-import pygame
-
-# Les lettres sont celles de la notation internationale du cube.
-# U haut, D bas, F devant, B derrière, L gauche, R droite.
-COULEURS = {
-    "U": (245, 245, 245), "D": (255, 215, 35),
-    "F": (35, 175, 95), "B": (50, 110, 235),
-    "L": (255, 145, 35), "R": (225, 55, 65),
-}
-
-
-def creer_cube():
-    # Chaque face contient trois lignes de trois lettres.
-    return {face: [[face for colonne in range(3)]
-                   for ligne in range(3)] for face in COULEURS}
-
-
-# Chaque groupe décrit quatre cases voisines qui échangent leur contenu.
-# Dans un groupe : la première va dans la deuxième, etc.
-# La dernière revient dans la première. Il y a trois groupes par face.
-VOISINS = {
-    "U": [
-        [('F', 0, 0), ('L', 0, 0), ('B', 0, 0), ('R', 0, 0)],
-        [('F', 0, 1), ('L', 0, 1), ('B', 0, 1), ('R', 0, 1)],
-        [('F', 0, 2), ('L', 0, 2), ('B', 0, 2), ('R', 0, 2)],
-    ],
-    "D": [
-        [('F', 2, 0), ('R', 2, 0), ('B', 2, 0), ('L', 2, 0)],
-        [('F', 2, 1), ('R', 2, 1), ('B', 2, 1), ('L', 2, 1)],
-        [('F', 2, 2), ('R', 2, 2), ('B', 2, 2), ('L', 2, 2)],
-    ],
-    "F": [
-        [('U', 2, 0), ('R', 0, 0), ('D', 0, 2), ('L', 2, 2)],
-        [('U', 2, 1), ('R', 1, 0), ('D', 0, 1), ('L', 1, 2)],
-        [('U', 2, 2), ('R', 2, 0), ('D', 0, 0), ('L', 0, 2)],
-    ],
-    "B": [
-        [('U', 0, 0), ('L', 2, 0), ('D', 2, 2), ('R', 0, 2)],
-        [('U', 0, 1), ('L', 1, 0), ('D', 2, 1), ('R', 1, 2)],
-        [('U', 0, 2), ('L', 0, 0), ('D', 2, 0), ('R', 2, 2)],
-    ],
-    "R": [
-        [('U', 0, 2), ('B', 2, 0), ('D', 0, 2), ('F', 0, 2)],
-        [('U', 1, 2), ('B', 1, 0), ('D', 1, 2), ('F', 1, 2)],
-        [('U', 2, 2), ('B', 0, 0), ('D', 2, 2), ('F', 2, 2)],
-    ],
-    "L": [
-        [('U', 0, 0), ('F', 0, 0), ('D', 0, 0), ('B', 2, 2)],
-        [('U', 1, 0), ('F', 1, 0), ('D', 1, 0), ('B', 1, 2)],
-        [('U', 2, 0), ('F', 2, 0), ('D', 2, 0), ('B', 0, 2)],
-    ],
-}
-
-
-def tourner(cube, face, inverse=False):
-    # Un tour inverse équivaut à trois tours dans le sens horaire.
-    repetitions = 3 if inverse else 1
-    for repetition in range(repetitions):
-        ancienne_face = cube[face]
-        cube[face] = [[ancienne_face[2 - colonne][ligne]
-                       for colonne in range(3)] for ligne in range(3)]
-        for groupe in VOISINS[face]:
-            valeurs = [cube[f][l][c] for f, l, c in groupe]
-            for indice, (f, l, c) in enumerate(groupe):
-                cube[f][l][c] = valeurs[indice - 1]
-
-
-def est_resolu(cube):
-    # Les centres restent fixes : chaque case doit retrouver sa face.
-    return all(case == face for face, lignes in cube.items()
-               for ligne in lignes for case in ligne)
-
-
-# --- NOTRE PETIT ATELIER 3D ---
-# X va vers la droite, Y vers le haut, Z vers le devant du cube.
-# Un vecteur est ici un groupe de trois nombres (x, y, z).
-NORMALES = {"U": (0, 1, 0), "D": (0, -1, 0),
-            "F": (0, 0, 1), "B": (0, 0, -1),
-            "R": (1, 0, 0), "L": (-1, 0, 0)}
-DROITES = {"U": (1, 0, 0), "D": (1, 0, 0),
-           "F": (1, 0, 0), "B": (-1, 0, 0),
-           "R": (0, 0, -1), "L": (0, 0, 1)}
-BAS = {"U": (0, 0, 1), "D": (0, 0, -1),
-       "F": (0, -1, 0), "B": (0, -1, 0),
-       "R": (0, -1, 0), "L": (0, -1, 0)}
-
-# À TOI : modifie un seul réglage à la fois.
-TITRE = "Mon Rubik's Cube en 3D"
-FOND = (22, 27, 40)
-DUREE_TOUR = 0.45          # Secondes : plus grand = plus lent.
-LONGUEUR_MELANGE = 20
-TAILLE_AUTOCOLLANT = 0.82 # Reste entre 0.5 et 0.9.
-
-
-def produit_scalaire(a, b):
-    return sum(x * y for x, y in zip(a, b))
-
-
-def produit_vectoriel(a, b):
-    return (a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2],
-            a[0]*b[1] - a[1]*b[0])
-
-
-def rotation(v, axe, angle):
-    # Formule de Rodrigues : tourner autour d'un axe de longueur 1.
-    # On peut l'utiliser comme un outil et l'étudier plus tard.
-    cosinus, sinus = math.cos(angle), math.sin(angle)
-    croix = produit_vectoriel(axe, v)
-    projection = produit_scalaire(axe, v)
-    return tuple(v[i]*cosinus + croix[i]*sinus
-                 + axe[i]*projection*(1 - cosinus) for i in range(3))
-
-
-def position_case(face, ligne, colonne):
-    return tuple(NORMALES[face][i] + (colonne - 1)*DROITES[face][i]
-                 + (ligne - 1)*BAS[face][i] for i in range(3))
-
-
-def autocollants(cube):
-    # On associe chaque carré de couleur à un petit cube et à sa normale.
-    resultat = {}
-    for face in cube:
-        for ligne in range(3):
-            for colonne in range(3):
-                cle = (position_case(face, ligne, colonne), NORMALES[face])
-                resultat[cle] = cube[face][ligne][colonne]
-    return resultat
-
-
-class Camera:
-    def __init__(self):
-        self.recentrer()
-
-    def recentrer(self):
-        self.lacet = -0.60
-        self.inclinaison = 0.48
-        self.zoom = 92
-
-    def orienter(self, vecteur):
-        v = rotation(vecteur, (0, 1, 0), self.lacet)
-        return rotation(v, (1, 0, 0), self.inclinaison)
-
-    def projeter(self, point):
-        x, y, z = self.orienter(point)
-        # Les objets proches paraissent plus grands : c'est la perspective.
-        facteur = self.zoom * 9 / (9 - z)
-        return (340 + x * facteur, 340 - y * facteur)
-
-    def glisser(self, dx, dy):
-        self.lacet += dx * 0.008
-        self.inclinaison += dy * 0.008
-
-    def agrandir(self, crans):
-        self.zoom = max(55, min(125, self.zoom + crans * 6))
-
-
-def transformation(point, position, animation):
-    # Seule la tranche concernée bouge ; le reste du cube reste immobile.
-    if animation and produit_scalaire(position, NORMALES[animation["face"]]) > 0.5:
-        fraction = min(1.0, animation["temps"] / animation["duree"])
-        souple = fraction * fraction * (3 - 2 * fraction)
-        signe = 1 if animation["inverse"] else -1
-        return rotation(point, NORMALES[animation["face"]], signe * math.pi/2 * souple)
-    return point
-
-
-def surfaces(cube, animation=None):
-    # Les 27 petits cubes ont un corps sombre et des autocollants extérieurs.
-    # On dessine aussi leurs faces internes : elles apparaissent pendant un tour.
-    couleurs = autocollants(cube)
-    resultat = []
-    for x in (-1, 0, 1):
-        for y in (-1, 0, 1):
-            for z in (-1, 0, 1):
-                position = (x, y, z)
-                for face, normale in NORMALES.items():
-                    lettre = couleurs.get((position, normale))
-                    tailles = [(0.94, 0.47, None)]
-                    if lettre:
-                        tailles.append((TAILLE_AUTOCOLLANT, 0.476, lettre))
-                    for taille, profondeur, etiquette in tailles:
-                        centre = tuple(position[i] + normale[i]*profondeur for i in range(3))
-                        coins = []
-                        for a, b in [(-1, -1), (1, -1), (1, 1), (-1, 1)]:
-                            point = tuple(centre[i] + taille/2 * (a*DROITES[face][i]
-                                          + b*BAS[face][i]) for i in range(3))
-                            coins.append(transformation(point, position, animation))
-                        n = transformation(normale, position, animation)
-                        centre = transformation(centre, position, animation)
-                        resultat.append((coins, n, centre, etiquette))
-    return resultat
-
-
-def dessiner_cube(fenetre, cube, camera, police, lettres=False, animation=None):
-    ancien_cadrage = fenetre.get_clip()
-    fenetre.set_clip(pygame.Rect(15, 85, 675, 515))
-    polygones = []
-    for coins, normale, centre, etiquette in surfaces(cube, animation):
-        normal_vue = camera.orienter(normale)
-        centre_vue = camera.orienter(centre)
-        vers_camera = (-centre_vue[0], -centre_vue[1], 9 - centre_vue[2])
-        if produit_scalaire(normal_vue, vers_camera) <= 0:
-            continue  # Une surface qui nous tourne le dos est cachée.
-        profondeur = sum(camera.orienter(p)[2] for p in coins) / 4
-        points = [camera.projeter(p) for p in coins]
-        if etiquette:
-            lumiere = 0.72 + 0.28 * max(0, produit_scalaire(normal_vue, (-0.3, 0.5, 0.81)))
-            couleur = tuple(min(255, int(c*lumiere)) for c in COULEURS[etiquette])
-        else:
-            couleur = (12, 15, 22)
-        polygones.append((profondeur, points, couleur, etiquette, camera.projeter(centre)))
-    # Peintre : les surfaces éloignées sont dessinées avant les surfaces proches.
-    for profondeur, points, couleur, etiquette, centre in sorted(polygones, key=lambda p: p[0]):
-        pygame.draw.polygon(fenetre, couleur, points)
-        if etiquette and lettres:
-            texte = police.render(etiquette, True, (12, 15, 20))
-            fenetre.blit(texte, texte.get_rect(center=(int(centre[0]), int(centre[1]))))
-    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 11")
-    horloge = pygame.time.Clock()
-    police = pygame.font.Font(None, 26)
-    petite = pygame.font.Font(None, 22)
-    titre = pygame.font.Font(None, 36)
-    camera = Camera()
-    glisser = False
-    lettres = False
-    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.extend([
-        (pygame.Rect(725, 415, 245, 36), "Mélanger", "melanger"),
-        (pygame.Rect(725, 461, 245, 36), "Remettre à zéro", "reset"),
-        (pygame.Rect(725, 507, 245, 36), "Annuler un coup", "annuler"),
-    ])
-    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
-    while en_marche:
-        secondes = min(horloge.tick(60) / 1000, 0.1)
-        for evenement in pygame.event.get():
-            action = None
-            if evenement.type == pygame.QUIT:
-                en_marche = False
-            elif evenement.type == pygame.MOUSEBUTTONDOWN and evenement.button == 1:
-                if evenement.pos[0] < 690:
-                    glisser = True
-                else:
-                    for rectangle, texte, commande in boutons:
-                        if rectangle.collidepoint(evenement.pos):
-                            action = commande
-            elif evenement.type == pygame.MOUSEBUTTONUP and evenement.button == 1:
-                glisser = False
-            elif evenement.type == pygame.MOUSEMOTION:
-                if glisser and evenement.buttons[0]:
-                    camera.glisser(*evenement.rel)
-                elif not evenement.buttons[0]:
-                    glisser = False
-            elif evenement.type == pygame.WINDOWFOCUSLOST:
-                glisser = False
-            elif evenement.type == pygame.MOUSEWHEEL:
-                camera.agrandir(evenement.y)
-            elif evenement.type == pygame.KEYDOWN:
-                if evenement.key == pygame.K_HOME:
-                    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))
-                elif evenement.key == pygame.K_SPACE:
-                    action = "melanger"
-                elif evenement.key == pygame.K_DELETE:
-                    action = "reset"
-                elif evenement.key == pygame.K_BACKSPACE:
-                    action = "annuler"
-            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, 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:
-            actif = rectangle.collidepoint(pygame.mouse.get_pos())
-            couleur = (76, 96, 140) if actif else (54, 68, 100)
-            pygame.draw.rect(fenetre, couleur, rectangle, border_radius=7)
-            etiquette = police.render(texte, True, (250, 250, 255))
-            fenetre.blit(etiquette, etiquette.get_rect(center=rectangle.center))
-        aides = ["Souris maintenue : tourner la vue", "Molette : zoomer",
-                 "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"]
-        aides += ["Espace : mélanger | Suppr : zéro", "Retour arrière : annuler"]
-        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))
-        fenetre.blit(petite.render(f"Tes coups : {partie.compteur}", True, (240,245,255)), (725, 681))
-        fenetre.blit(petite.render(partie.message, True, (240,245,255)), (30, 716))
-        pygame.display.flip()
-    pygame.quit()
-
+from atelier_cube_reel import demarrer
 
 if __name__ == "__main__":
-    main()
+    demarrer(etape=12)
