# ################################## # Pipopipette # Version 8 # nsi.gecif.net # Juin 2026 # ################################## """ Pour permettre d'annuler les deux derniers coups (le coup joué par l'IA et le coup précédent de l'Humain), nous devons mettre en place un système d'historique. Chaque fois qu'une ligne est tracée, nous allons enregistrer dans une liste (une pile) quelle était la ligne, qui l'a tracée, et si ce coup a provoqué la fermeture d'un carré. Lors d'un clic sur le bouton "Annuler", le programme va dépiler les deux dernières actions, effacer le plateau de jeu graphique, puis redessiner uniquement les lignes et carrés restants. """ import turtle import random # --- CONFIGURATION ET VARIABLES GLOBALES --- NB_DOTS = 5 ESPACE = 60 ORIGINE_X = 0 ORIGINE_Y = 0 joueur_actuel = "Humain" score = {"Humain": 0, "Ordinateur": 0} lignes_tracees = set() carres_valides = set() # NOUVEAU : Historique des coups pour la fonction Annuler # Contiendra des dictionnaires : {"ligne": ..., "joueur": ..., "carres_fermes": [(cx, cy), ...]} historique_coups = [] # Coordonnées des boutons graphiques BTN_REC_X1, BTN_REC_Y1 = 220, 330 BTN_REC_X2, BTN_REC_Y2 = 380, 365 BTN_ANN_X1, BTN_ANN_Y1 = 40, 330 BTN_ANN_X2, BTN_ANN_Y2 = 200, 365 # --- FONCTIONS DE CONFIGURATION ET TRACÉ --- def configurer_partie(): """Demande la taille, le premier joueur et réinitialise le jeu.""" global NB_DOTS, ORIGINE_X, ORIGINE_Y, joueur_actuel, score, lignes_tracees, carres_valides, historique_coups taille = turtle.numinput("Configuration", "Taille de la grille (de 5 à 12 points) :", default=5, minval=5, maxval=12) if taille is None: NB_DOTS = 5 else: NB_DOTS = int(taille) choix = turtle.textinput("Configuration", "Qui commence ? Entrez 'H' pour Humain ou 'I' pour IA :") if choix and choix.strip().upper() == "I": joueur_actuel = "Ordinateur" else: joueur_actuel = "Humain" ORIGINE_X = -((NB_DOTS - 1) * ESPACE) / 2 ORIGINE_Y = ((NB_DOTS - 1) * ESPACE) / 2 - 40 score = {"Humain": 0, "Ordinateur": 0} lignes_tracees = set() carres_valides = set() historique_coups = [] stylo.clear() dessiner_grille() dessiner_boutons() mettre_a_jour_tableau_scores() if joueur_actuel == "Ordinateur": turtle.ontimer(jouer_ordinateur, 600) def coord_vers_pixel(cx, cy): px = ORIGINE_X + (cx * ESPACE) py = ORIGINE_Y - (cy * ESPACE) return px, py def dessiner_grille(): turtle.tracer(0) for r in range(NB_DOTS): for c in range(NB_DOTS): px, py = coord_vers_pixel(c, r) stylo.penup() stylo.goto(px, py) stylo.pendown() stylo.dot(10, "#333333") turtle.update() def dessiner_boutons(): """Dessine les boutons 'Recommencer' et 'Annuler'.""" turtle.tracer(0) stylo_score.penup() # 1. Bouton Recommencer stylo_score.goto(BTN_REC_X1, BTN_REC_Y1) stylo_score.pendown() stylo_score.color("black", "#e0e0e0") stylo_score.begin_fill() for _ in range(2): stylo_score.forward(BTN_REC_X2 - BTN_REC_X1) stylo_score.left(90) stylo_score.forward(BTN_REC_Y2 - BTN_REC_Y1) stylo_score.left(90) stylo_score.end_fill() stylo_score.penup() stylo_score.goto((BTN_REC_X1 + BTN_REC_X2) / 2, BTN_REC_Y1 + 7) stylo_score.write("Recommencer", align="center", font=("Arial", 10, "bold")) # 2. ÉVOLUTION : Bouton Annuler stylo_score.penup() stylo_score.goto(BTN_ANN_X1, BTN_ANN_Y1) stylo_score.pendown() stylo_score.color("black", "#ffd1d1") stylo_score.begin_fill() for _ in range(2): stylo_score.forward(BTN_ANN_X2 - BTN_ANN_X1) stylo_score.left(90) stylo_score.forward(BTN_ANN_Y2 - BTN_ANN_Y1) stylo_score.left(90) stylo_score.end_fill() stylo_score.penup() stylo_score.goto((BTN_ANN_X1 + BTN_ANN_X2) / 2, BTN_ANN_Y1 + 7) stylo_score.write("Annuler coup", align="center", font=("Arial", 10, "bold")) turtle.update() def mettre_a_jour_tableau_scores(): turtle.tracer(0) stylo_score.penup() # Zone de nettoyage dynamique pour les scores uniquement (sans toucher aux boutons) stylo_score.color("white") stylo_score.begin_fill() stylo_score.goto(-380, ORIGINE_Y + 30) for _ in range(2): stylo_score.forward(410) stylo_score.left(90) stylo_score.forward(40) stylo_score.left(90) stylo_score.end_fill() stylo_score.penup() stylo_score.goto(-180, ORIGINE_Y + 40) stylo_score.color("red" if joueur_actuel == "Humain" else "blue") texte = "Tour : {0} | Score -> Humain : {1} - IA : {2}" stylo_score.write(texte.format(joueur_actuel, score["Humain"], score["Ordinateur"]), align="center", font=("Arial", 12, "bold")) turtle.update() def redessiner_plateau_complet(): """Efface la grille graphique et reconstruit tout à partir des listes de données.""" turtle.tracer(0) stylo.clear() # Redessine les points dessiner_grille() # Redessine les carrés validés restants for (cx, cy) in carres_valides: # Retrouver quel joueur possédait ce carré dans l'historique couleur_carre = "Humain" for coup in historique_coups: if (cx, cy) in coup["carres_fermes"]: couleur_carre = coup["joueur"] px, py = coord_vers_pixel(cx, cy) stylo.penup() stylo.goto(px + 4, py - 4) stylo.color("#ffb3b3" if couleur_carre == "Humain" else "#b3d1ff") stylo.begin_fill() for _ in range(4): stylo.forward(ESPACE - 8) stylo.right(90) stylo.end_fill() # Redessine les lignes restantes for ligne in lignes_tracees: # Trouver la couleur de la ligne originale couleur_ligne = "Humain" for coup in historique_coups: if coup["ligne"] == ligne: couleur_ligne = coup["joueur"] p1_px, p1_py = coord_vers_pixel(ligne[0][0], ligne[0][1]) p2_px, p2_py = coord_vers_pixel(ligne[1][0], ligne[1][1]) stylo.penup() stylo.goto(p1_px, p1_py) stylo.pendown() stylo.pensize(5) stylo.color("red" if couleur_ligne == "Humain" else "blue") stylo.goto(p2_px, p2_py) turtle.update() def verifier_carrés_fermes(p1, p2, couleur): """Vérifie si des carrés se ferment et retourne la liste des coordonnées des carrés fermés.""" global score x1, y1 = p1 x2, y2 = p2 carres_fermes_ce_coup = [] carrés_a_tester = [] if y1 == y2: cx = min(x1, x2) carrés_a_tester.append((cx, y1 - 1)) carrés_a_tester.append((cx, y1)) else: cy = min(y1, y2) carrés_a_tester.append((x1 - 1, cy)) carrés_a_tester.append((x1, cy)) for (cx, cy) in carrés_a_tester: if 0 <= cx < NB_DOTS - 1 and 0 <= cy < NB_DOTS - 1: if (cx, cy) not in carres_valides: l_haut = ((cx, cy), (cx + 1, cy)) l_bas = ((cx, cy + 1), (cx + 1, cy + 1)) l_gauche = ((cx, cy), (cx, cy + 1)) l_droite = ((cx + 1, cy), (cx + 1, cy + 1)) if (l_haut in lignes_tracees and l_bas in lignes_tracees and l_gauche in lignes_tracees and l_droite in lignes_tracees): carres_valides.add((cx, cy)) carres_fermes_ce_coup.append((cx, cy)) score[couleur] += 1 return carres_fermes_ce_coup def tracer_ligne_graphique(ligne, couleur): p1_px, p1_py = coord_vers_pixel(ligne[0][0], ligne[0][1]) p2_px, p2_py = coord_vers_pixel(ligne[1][0], ligne[1][1]) turtle.tracer(0) stylo.penup() stylo.goto(p1_px, p1_py) stylo.pendown() stylo.pensize(5) stylo.color("red" if couleur == "Humain" else "blue") stylo.goto(p2_px, p2_py) turtle.update() # --- NOUVEAU : LOGIQUE D'ANNULATION DES COUPS --- def annuler_dernier_tour(): """Supprime le dernier coup de l'IA et le coup précédent de l'Humain.""" global joueur_actuel, score if not historique_coups: return # On doit annuler par paires (Coup IA + Coup Humain) # Sauf si l'IA a commencé et qu'aucun humain n'a encore joué nb_coups_a_retirer = 2 if len(historique_coups) >= 2 else 1 for _ in range(nb_coups_a_retirer): coup_supprime = historique_coups.pop() # 1. Retirer la ligne des structures de données if coup_supprime["ligne"] in lignes_tracees: lignes_tracees.remove(coup_supprime["ligne"]) # 2. Retirer les carrés fermés par ce coup et soustraire les points for (cx, cy) in coup_supprime["carres_fermes"]: if (cx, cy) in carres_valides: carres_valides.remove((cx, cy)) score[coup_supprime["joueur"]] -= 1 # Redonner le contrôle à l'Humain joueur_actuel = "Humain" # Actualisation visuelle globale redessiner_plateau_complet() mettre_a_jour_tableau_scores() # --- INTELLIGENCE ARTIFICIELLE (IA) --- def lister_lignes_restantes(): dispo = [] for r in range(NB_DOTS): for c in range(NB_DOTS): if c < NB_DOTS - 1: l = ((c, r), (c + 1, r)) if l not in lignes_tracees: dispo.append(l) if r < NB_DOTS - 1: l = ((c, r), (c, r + 1)) if l not in lignes_tracees: dispo.append(l) return dispo def compter_cotes_carre(cx, cy): l_haut = ((cx, cy), (cx + 1, cy)) l_bas = ((cx, cy + 1), (cx + 1, cy + 1)) l_gauche = ((cx, cy), (cx, cy + 1)) l_droite = ((cx + 1, cy), (cx + 1, cy + 1)) compte = 0 if l_haut in lignes_tracees: compte += 1 if l_bas in lignes_tracees: compte += 1 if l_gauche in lignes_tracees: compte += 1 if l_droite in lignes_tracees: compte += 1 return compte def jouer_ordinateur(): global joueur_actuel lignes_dispo = lister_lignes_restantes() if not lignes_dispo: return ligne_choisie = None # STRATÉGIE 1 : ATTAQUE for l in lignes_dispo: lignes_tracees.add(l) x1, y1 = l[0]; x2, y2 = l[1] carrés_potentiels = [(min(x1, x2), y1 - 1), (min(x1, x2), y1)] if y1 == y2 else [(x1 - 1, min(y1, y2)), (x1, min(y1, y2))] for (cx, cy) in carrés_potentiels: if 0 <= cx < NB_DOTS - 1 and 0 <= cy < NB_DOTS - 1: if compter_cotes_carre(cx, cy) == 4 and (cx, cy) not in carres_valides: ligne_choisie = l lignes_tracees.remove(l) if ligne_choisie: break # STRATÉGIE 2 : DÉFENSE if not ligne_choisie: lignes_sures = [] for l in lignes_dispo: donne_opportunite = False lignes_tracees.add(l) x1, y1 = l[0]; x2, y2 = l[1] carrés_potentiels = [(min(x1, x2), y1 - 1), (min(x1, x2), y1)] if y1 == y2 else [(x1 - 1, min(y1, y2)), (x1, min(y1, y2))] for (cx, cy) in carrés_potentiels: if 0 <= cx < NB_DOTS - 1 and 0 <= cy < NB_DOTS - 1: if compter_cotes_carre(cx, cy) == 3: donne_opportunite = True lignes_tracees.remove(l) if not donne_opportunite: lignes_sures.append(l) if lignes_sures: ligne_choisie = random.choice(lignes_sures) # STRATÉGIE 3 : PAR DÉFAUT if not ligne_choisie: ligne_choisie = random.choice(lignes_dispo) lignes_tracees.add(ligne_choisie) # Enregistrement du coup de l'IA dans l'historique avant tracé carres_fermes = verifier_carrés_fermes(ligne_choisie[0], ligne_choisie[1], "Ordinateur") historique_coups.append({"ligne": ligne_choisie, "joueur": "Ordinateur", "carres_fermes": carres_fermes}) redessiner_plateau_complet() if carres_fermes: mettre_a_jour_tableau_scores() verifier_fin_de_partie() turtle.ontimer(jouer_ordinateur, 500) else: joueur_actuel = "Humain" mettre_a_jour_tableau_scores() def verifier_fin_de_partie(): total_carres_possibles = (NB_DOTS - 1) * (NB_DOTS - 1) if len(carres_valides) == total_carres_possibles: stylo_score.penup() stylo_score.goto(-180, ORIGINE_Y + 40) stylo_score.color("black") if score["Humain"] == score["Ordinateur"]: res = "FIN : Égalité !" else: gagnant = "Humain" if score["Humain"] > score["Ordinateur"] else "L'IA" res = "FIN : Victoire de {0} !".format(gagnant) stylo_score.write(res, align="center", font=("Arial", 12, "bold")) turtle.update() # --- GESTION DU CLIC --- def au_clic(px, py): global joueur_actuel # Clic Bouton Recommencer if BTN_REC_X1 <= px <= BTN_REC_X2 and BTN_REC_Y1 <= py <= BTN_REC_Y2: stylo_score.clear() configurer_partie() return # Clic Bouton Annuler if BTN_ANN_X1 <= px <= BTN_ANN_X2 and BTN_ANN_Y1 <= py <= BTN_ANN_Y2: annuler_dernier_tour() return if joueur_actuel != "Humain": return if len(carres_valides) == (NB_DOTS - 1) * (NB_DOTS - 1): return distance_min = 15 ligne_choisie = None for r in range(NB_DOTS): for c in range(NB_DOTS): if c < NB_DOTS - 1: x1, y1 = coord_vers_pixel(c, r); x2, y2 = coord_vers_pixel(c + 1, r) dist = ((px - (x1+x2)/2)**2 + (py - (y1+y2)/2)**2)**0.5 if dist < distance_min: distance_min = dist; ligne_choisie = ((c, r), (c + 1, r)) if r < NB_DOTS - 1: x1, y1 = coord_vers_pixel(c, r); x2, y2 = coord_vers_pixel(c, r + 1) dist = ((px - (x1+x2)/2)**2 + (py - (y1+y2)/2)**2)**0.5 if dist < distance_min: distance_min = dist; ligne_choisie = ((c, r), (c, r + 1)) if ligne_choisie and (ligne_choisie not in lignes_tracees): lignes_tracees.add(ligne_choisie) # Enregistrement du coup de l'Humain dans l'historique carres_fermes = verifier_carrés_fermes(ligne_choisie[0], ligne_choisie[1], "Humain") historique_coups.append({"ligne": ligne_choisie, "joueur": "Humain", "carres_fermes": carres_fermes}) redessiner_plateau_complet() if not carres_fermes: joueur_actuel = "Ordinateur" mettre_a_jour_tableau_scores() verifier_fin_de_partie() turtle.ontimer(jouer_ordinateur, 500) else: mettre_a_jour_tableau_scores() verifier_fin_de_partie() # --- INITIALISATION --- turtle.setup(width=800, height=800, startx=0, starty=0) turtle.title("La Pipopipette vs Ordinateur") stylo = turtle.Turtle(); stylo.hideturtle(); stylo.speed(0) stylo_score = turtle.Turtle(); stylo_score.hideturtle(); stylo_score.speed(0) configurer_partie() turtle.onscreenclick(au_clic) turtle.listen() turtle.mainloop()