from math import sqrt # Utile pour avoir dynamiquement le nombre de cases a validé pour gagner une partie class Minimax: """Définition de l'algorithme minimax.""" def __init__(self, terminale: list, humain: str, ordinateur: str) -> None: self.terminale = terminale self.MIN = humain self.MAX = ordinateur def evaluation(self, nbAlign: list) -> list: """ `p` -> position `nbAlign_i(p, J)` -> le nombre d'alignements réalisables, par le joueur `J`, pour lesquels il a déjà `i` symboles posés. f(p) = (3.nbAlign_2(p, humain) + nbAlign_1(p, humain)) - (3.nbAlign_2(p, ordinateur) + nbAlign_1(p, ordinateur)) """ pass def terminale(self, etat: list) -> bool: """Vrai si la partie est terminé.""" return etat in self.terminale def casesVide(self, p: list) -> list: # c'est une liste de coordonées """Renvoie la liste des cellules vides depuis un état.""" pass def algorithme(self, n: int, p: list, j: str) -> list: # p state, n depth, j player """Évaluation de `p` à une profondeur `n` (joueur j).""" if self.terminale(p) or n == 0: # si p terminale ou n = 0 f = self.evaluation(p) else: # = si n > 0 for Pm in self.casesVide(p): x, y = Pm[0], Pm[1] p[x][y] = j score = self.main(p, n - 1, -j) p[x][y] = 0 score[0], score[1] = x, y if j == self.MAX: if score[2] > f[2]: f = score else: if score[2] < f[2]: f = score return f class Morpion(Minimax): """ Implémentation de Minimax dans un Morpion. Taille par défaut : `3x3`. Joueur A est l'humain. Joueur B est l'ordinateur. """ def __init__(self, joueurA: str, joueurB: str, taille: tuple = (3, 3)) -> None: self.plateau: list = self._definitionPlateau(taille[0], taille[1]) if len(self.plateau) == 0: # Gestion erreur du plateau print("Taille du plateau invalide.") return self.nbCasesGagnantes = self._recuperationNbCasesGagnantes() if len(joueurA) != 1 or len(joueurB) != 1: # Gestion erreur nom des joueurs print("Nom des joueurs invalide.") return super().__init__(self._recuperationEtatsGagnants(), joueurA, joueurB) self.joueurA = joueurA self.joueurB = joueurB self.gagnant = '' def _definitionPlateau(self, x: int, y: int) -> list: """ Renvoie un plateau en fonction de dimensions `x` et `y` Valeur = int -> Cellule vide """ if x <= 0 or y <= 0: return [] plateau = [] numCase = 1 for i in range(y): plateau.append([]) for _ in range(x): plateau[i].append(numCase) numCase += 1 return plateau def _recuperationNbCasesGagnantes(self) -> int: """ Renvoie le nombre de cases à aligné pour pouvoir gagner la partie. Défini en fonction de la taille du plateau. """ mini = len(self.plateau) # mini correspond au côté le plus petit if len(self.plateau[0]) < mini: mini = len(self.plateau[0]) res = round(sqrt(mini) * 2) # fonction qui grandit lentement, basé sur https://www.mathsisfun.com/sets/functions-common.html if res > mini: # si res trouvé excède la taille du côté, alors c'est autant que la taille du côté res = mini return res def _recuperationEtatsGagnants(self) -> list: """Renvoie une liste de tous les états qui permettent de terminer la partie.""" self.plateau pass def afficher(self) -> None: """Affiche le plateau.""" print("\n" * 5) # petit espace par rapport à ce que le terminale de l'utilisateur à afficher dernièrement espace = f"\n {'-' * len(self.plateau[0]) * 5}-" # taille des lignes qui séparent les valeurs du plateau for i in range(len(self.plateau)): print(f"{espace[1:] if i == 0 else espace}", end = "\n ") # on retire le saut de ligne à la première itération print("", end = "| ") for j in range(len(self.plateau[i])): n = self.plateau[i][j] if type(n) == int: print(f"{n:02d}", end = " | ") # on rajoute un "0" au chiffre < 10 (comme en C avec `%02d`) else: print("%02c" % n, end = " | ") # espace automatique pour pas décaler l'affichage (comme au dessus mais avec la syntaxe de python2) print(espace) def _caseOccupee(self, x: int, y: int) -> bool: """Vrai si la case en `x` et `y` est déjà occupée par un joueur.""" return type(self.plateau[x][y]) == str def _terminer(self) -> bool: """Vrai si la partie est terminée.""" fini = False for i in range(len(self.plateau)): for j in range(len(self.plateau[i])): if self._caseOccupee(i, j): # si case occupé par un joueur rechercheLigne = 0 rechercheColonne = 0 # les diagonales vont vers la droite rechercheDiagonaleVersBas = 0 rechercheDiagonaleVersHaut = 0 joueur = self.plateau[i][j] while (rechercheLigne != -1) or (rechercheColonne != -1) or (rechercheDiagonaleVersBas != -1) or (rechercheDiagonaleVersHaut != -1): # -- recherche en ligne -- if len(self.plateau[i]) - j >= self.nbCasesGagnantes: # s'il y a techniquement assez de cases devant pour gagner if self.plateau[i][j + rechercheLigne] == joueur: rechercheLigne += 1 else: # si l'élément ne correspond pas au joueur c'est que il n'a pas gagné depuis cette case avec cette direction rechercheLigne = -1 else: # sinon c'est que ça sert de continuer pour cette ligne rechercheLigne = -1 if self.nbCasesGagnantes == rechercheLigne: # si on a trouvé autant de cases à la suite du même joueur qu'il en faut pour gagner fini = True break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while # -- recherche en colonne -- if len(self.plateau) - i >= self.nbCasesGagnantes: # s'il y a techniquement assez de cases devant pour gagner if self.plateau[i + rechercheColonne][j] == joueur: rechercheColonne += 1 else: # si l'élément ne correspond pas au joueur c'est que il n'a pas gagné depuis cette case avec cette direction rechercheColonne = -1 else: # sinon c'est que ça sert de continuer pour cette ligne rechercheColonne = -1 if self.nbCasesGagnantes == rechercheColonne: # si on a trouvé autant de cases à la suite du même joueur qu'il en faut pour gagner fini = True break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while # -- recherche en diagonale vers le bas -- if (len(self.plateau) - i >= self.nbCasesGagnantes) and (len(self.plateau[i]) - j >= self.nbCasesGagnantes): # s'il y a techniquement assez de cases devant pour gagner if self.plateau[i + rechercheDiagonaleVersBas][j + rechercheDiagonaleVersBas] == joueur: rechercheDiagonaleVersBas += 1 else: # si l'élément ne correspond pas au joueur c'est que il n'a pas gagné depuis cette case avec cette direction rechercheDiagonaleVersBas = -1 else: # sinon c'est que ça sert de continuer pour cette ligne rechercheDiagonaleVersBas = -1 if self.nbCasesGagnantes == rechercheDiagonaleVersBas: # si on a trouvé autant de cases à la suite du même joueur qu'il en faut pour gagner fini = True break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while # -- recherche en diagonale vers le haut -- if (len(self.plateau) - self.nbCasesGagnantes >= 0) and (len(self.plateau[i]) - j >= self.nbCasesGagnantes): # s'il y a techniquement assez de cases devant pour gagner idxI = i - rechercheDiagonaleVersHaut # i ne peut pas être <= 0 if idxI >= 0 and self.plateau[idxI][j + rechercheDiagonaleVersHaut] == joueur: rechercheDiagonaleVersHaut += 1 else: # si l'élément ne correspond pas au joueur c'est que il n'a pas gagné depuis cette case avec cette direction rechercheDiagonaleVersHaut = -1 else: # sinon c'est que ça sert de continuer pour cette ligne rechercheDiagonaleVersHaut = -1 if self.nbCasesGagnantes == rechercheDiagonaleVersHaut: # si on a trouvé autant de cases à la suite du même joueur qu'il en faut pour gagner fini = True break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while if fini: # meme chose, on quitte la boucle for (j) si on a fini break if fini: # meme chose, on quitte la boucle for (i) si on a fini break if fini: self.gagnant = joueur return fini def _coordonneesCaseDepuisNumero(self, numero: int) -> tuple[int, int]: """Renvoie les coordonnées d'une case.""" return ((numero - 1) // len(self.plateau[0]), (numero - 1) % len(self.plateau[0])) def _placementPiece(self, joueur: str, n: int) -> None: """Place la pièce d'un joueur dans le plateau.""" x, y = self._coordonneesCaseDepuisNumero(n) self.plateau[x][y] = joueur def _demandeCase(self) -> int: """Demande au joueur sur quelle case il veut poser sa pièce.""" prefix = f"({self.joueurA} - {self.nbCasesGagnantes} cases successives pour gagner)" print(f"{prefix} Entrez le numéro de case que vous voulez jouer : ", end = "") reponse = -1 while int(reponse) < 0: erreur = "" reponse = input() if not reponse.isnumeric(): erreur = "pas un nombre" elif int(reponse) > len(self.plateau) * len(self.plateau[0]): erreur = "valeur trop grande" elif int(reponse) == 0: # < 0 pas considéré comme un nombre avec la fonction `isnumeric` erreur = "valeur trop petite" else: x, y = self._coordonneesCaseDepuisNumero(int(reponse)) if self._caseOccupee(x, y): erreur = "case déjà occupée" if len(erreur) > 0: print(f"{prefix} ❌ Valeur invalide ({erreur}), réessayez : ", end = "") reponse = -1 return int(reponse) def gras(self, texte: str) -> str: """Fonction qui renvoie le texte en argument en gras.""" return f"\033[1m{texte}\033[0m" def jouer(self) -> None: """Lance la partie de Morpion.""" while not self._terminer(): # tant que la partie n'est pas terminé self.afficher() # affichage du plateau reponse = self._demandeCase() # on demande où le joueur veut posé sa pièce self._placementPiece(self.joueurA, reponse) # on place la pièce du joueur # self._placementPiece(self.joueurB, self.algorithme()) self.afficher() # affichage du plateau final print(f"🎉 Partie terminée, le {self.gras(f'joueur {self.gagnant}')} a gagné !") if __name__ == "__main__": # Si on lance directement le fichier et on s'en sert pas comme module Morpion('X', 'O').jouer() # On lance la partie à l'instanciation du Morpion # Morpion('X', 'O', (4, 4)).jouer() # Si on veut lancer le morpion avec un plateau 4x4