# ################################## # Pipopipette # Version 9 # nsi.gecif.net # Juin 2026 # ################################## """ Problème : si l'IA ou l'humain a gagné le bouton "Annuler" doit annuler plus de 2 coups, or ce n'était pas le cas avec la version 8. Le problème vient du fait que le jeu de la Pipopipette accorde un tour bonus chaque fois qu'un joueur ferme un carré. Si l'humain ou l'IA enchaînez plusieurs carrés d'affilée, un seul et même "tour" de jeu peut en réalité contenir 3, 4 ou 5 coups successifs. Pour que le bouton "Annuler" efface correctement toute la séquence jusqu'au moment où l'Humain avait la main pour la dernière fois, il ne faut pas bêtement retirer 2 coups. Il faut remonter dans l'historique et annuler tous les coups successifs de l'IA, puis continuer d'annuler les coups de l'Humain jusqu'à tomber sur le tout premier trait qu'il a tracé. Voici le code corrigé et optimisé pour Python 3.2.5 : """ 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() historique_coups = [] # Coordonnées des boutons graphiques BTN_REC_X1, BTN_REC_Y1 = 220, 300 BTN_REC_X2, BTN_REC_Y2 = 380, 335 BTN_ANN_X1, BTN_ANN_Y1 = 40, 300 BTN_ANN_X2, BTN_ANN_Y2 = 200, 335 # --- FONCTIONS DE CONFIGURATION ET TRACÉ --- def configurer_partie(): 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(): turtle.tracer(0) stylo_score.penup() # 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")) # 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() 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(): turtle.tracer(0) stylo.clear() dessiner_grille() for (cx, cy) in carres_valides: 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() for ligne in lignes_tracees: 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): 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 # --- CORRECTION DE LA LOGIQUE D'ANNULATION --- def annuler_dernier_tour(): """Annule toute la séquence de coups jusqu'au début du dernier tour de l'Humain.""" global joueur_actuel, score if not historique_coups: return # Étape 1 : On commence par annuler TOUS les coups consécutifs de l'Ordinateur # se trouvant en haut de la pile de l'historique while historique_coups and historique_coups[-1]["joueur"] == "Ordinateur": coup = historique_coups.pop() lignes_tracees.remove(coup["ligne"]) for (cx, cy) in coup["carres_fermes"]: carres_valides.remove((cx, cy)) score["Ordinateur"] -= 1 # Étape 2 : On doit maintenant annuler la série de coups de l'Humain # qui a mené à cette situation (son dernier tour de jeu complet) # On retire ses coups tant qu'il fermait des carrés (coups bonus) humain_a_annule_son_premier_coup = False while historique_coups and historique_coups[-1]["joueur"] == "Humain" and not humain_a_annule_son_premier_coup: # Si le coup sous nos yeux n'a généré aucun carré, c'était le coup initial de son tour ! if not historique_coups[-1]["carres_fermes"]: humain_a_annule_son_premier_coup = True coup = historique_coups.pop() lignes_tracees.remove(coup["ligne"]) for (cx, cy) in coup["carres_fermes"]: carres_valides.remove((cx, cy)) score["Humain"] -= 1 # On s'assure de redonner la main à l'Humain joueur_actuel = "Humain" 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 # Sécurité si l'utilisateur clique sur Annuler pendant la réflexion de l'IA if joueur_actuel != "Ordinateur": return 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) 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 (Fonctionne même si la partie est théoriquement finie !) 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) 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()