diff --git a/minimax.py b/minimax.py index bb6bebb..637bb88 100644 --- a/minimax.py +++ b/minimax.py @@ -1,80 +1,40 @@ -from math import sqrt # Utile pour avoir dynamiquement le nombre de cases a validé pour gagner une partie +from math import sqrt, inf +from random import choice -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: +class Morpion(): + """Implémentation du Morpion.""" + def __init__(self, joueurA: str = 'X', joueurB: str = 'O', taille: tuple = (3, 3)) -> None: """ - `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)) + Initalise la classe Morpion : + - Par défaut : + - Le joueur A se nomme `X`. + - Le joueur B se nomme `O`. + - Le plateau à une taille `3x3`. """ - 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: + if len(joueurA) != 1 or len(joueurB) != 1: # Gestion erreur nom des joueurs + print("Nom des joueurs invalide.") + return 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 = '' + self.nbCasesGagnantes = self._recuperationNbCasesGagnantes() # définit combien de cases les joueurs doivent aligner pour gagner + self.joueurA = joueurA # définit le joueur A + self.joueurB = joueurB # définit le joueur B + self.joueurActuel = choice([joueurA, joueurB]) # le joueur qui commence est choisi aléatoirement car le nom 'X' peut varier + self.gagnant = '' # à la fin, montre qui est le gagnant + """ + Permet un bel affichage, la variable `tailleMaxCharactere` augmente en fonction de la taille du tableau et permet + de toujours avoir tout de centrer et tout de la bonne longueur + """ + tailleMaxCharactere = len(str(self.plateau[-1][-1])) + self.tailleMaxCharactere = tailleMaxCharactere if tailleMaxCharactere % 2 != 0 else tailleMaxCharactere + 1 def _definitionPlateau(self, x: int, y: int) -> list: """ - Renvoie un plateau en fonction de dimensions `x` et `y` + Renvoie un plateau en fonction de dimensions `x` et `y`. - Valeur = int -> Cellule vide + Les cellules vides sont correspondent à leur "numéro de case". """ if x <= 0 or y <= 0: return [] @@ -103,24 +63,20 @@ class Morpion(Minimax): 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 + espace = f"\n {'-' * len(self.plateau[0]) * (3 + self.tailleMaxCharactere)}-" # 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`) + print(f"{n:0{self.tailleMaxCharactere}d}", 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) + nombreEspaceRequis = (self.tailleMaxCharactere - 1) // 2 + print(f"{' ' * nombreEspaceRequis}{n}{' ' * nombreEspaceRequis}", end = " | ") # espace automatique pour pas décaler l'affichage print(espace) def _caseOccupee(self, x: int, y: int) -> bool: @@ -211,9 +167,9 @@ class Morpion(Minimax): x, y = self._coordonneesCaseDepuisNumero(n) self.plateau[x][y] = joueur - def _demandeCase(self) -> int: + def _demandeCase(self, joueur) -> int: """Demande au joueur sur quelle case il veut poser sa pièce.""" - prefix = f"({self.joueurA} - {self.nbCasesGagnantes} cases successives pour gagner)" + prefix = f"({joueur} - {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: @@ -236,19 +192,82 @@ class Morpion(Minimax): return int(reponse) def gras(self, texte: str) -> str: - """Fonction qui renvoie le texte en argument en gras.""" + """Fonction qui renvoie le texte en argument en gras.""" # source: https://stackoverflow.com/a/17303428 return f"\033[1m{texte}\033[0m" + def _demandeCaseA(self) -> int: + """Demande au joueur A de jouer.""" + return self._demandeCase(self.joueurA) + + def _demandeCaseB(self) -> int: + """Demande au joueur B de jouer.""" + return self._demandeCase(self.joueurB) + + def _egalite(self) -> bool: + """Renvoie vrai si le plateau est plein.""" + for i in range(len(self.plateau)): + for j in range(len(self.plateau[i])): + if not self._caseOccupee(i, j): # si case pas occupée par un joueur + return False + return True + def jouer(self) -> None: """Lance la partie de Morpion.""" - while not self._terminer(): # tant que la partie n'est pas terminé + while not self._terminer() and not self._egalite(): # 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()) + if self.joueurActuel == self.joueurA: + self._placementPiece(self.joueurA, self._demandeCaseA()) # on place la pièce du joueur là où il veut + self.joueurActuel = self.joueurB + else: + self._placementPiece(self.joueurB, self._demandeCaseB()) # on place la pièce du joueur là où il veut + self.joueurActuel = self.joueurA self.afficher() # affichage du plateau final - print(f"🎉 Partie terminée, le {self.gras(f'joueur {self.gagnant}')} a gagné !") + if self._egalite(): + print(f"😬 Partie terminée, {self.gras(f'égalité parfaite')}.") + else: + print(f"🎉 Partie terminée, le {self.gras(f'joueur {self.gagnant} a gagné')} !") + +class Minimax(Morpion): + """Définition de l'algorithme Minimax.""" + def __init__(self, joueurA: str = 'X', joueurB: str = 'O', taille: tuple = (3, 3)) -> None: + """ + Initalise la classe Minimax héritant du Morpion. + - Taille par défaut : `3x3`. + - Joueur A est l'humain. + - Joueur B est l'ordinateur. + """ + super().__init__(joueurA, joueurB, taille = taille) + + def _demandeCaseB(self) -> int: + """Utilise l'algorithme `Minimax` pour jouer le coup du joueur B.""" + return self.minimax() + + def minimax(self) -> list: + """ + Fonction Minimax qui décide quel case est la plus intéressante. + """ + return super()._demandeCaseB() 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 + """ + J'ai fait 2 classes : + -> La classe Morpion permet de jouer au morpion entre deux humains. + -> Dans mon morpion : + -> `X` et `O` complètement personnalisable. + -> La taille du plateau peut s'étendre à l'infini. + -> Le joueur qui commence est choisit aléatoirement car `X` n'est pas obligatoirement le joueur A. + -> Pour rendre les parties avec de grands plateaux quand même intéréssant, + j'ai fait en sorte (avec la racine du côté le plus petit multiplié par 2) + de ne pas donner comme condition de victoire toutes une longueur ou une largeur + complété mais seulement un morceau. Ce morceau grossit en fonction de la taille du plateau. + -> La classe Minimax dépend de Morpion (mais elle peut être rataché à d'autre classe ce qui la rend extrêmement flexible) + et permet de remplacer le joueur B et ainsi jouer contre l'algorithme `Minimax`. + + Minimax hérite donc des arguments de Morpion : + -> Pour configurer le morpion on peut lui donner : + -> Le nom du joueur A. + -> Le nom du joueur B. + -> Une taille de plateau (qui peut ne pas être identique, exemple : un plateau de 4x6). + -> Les noms de joueurs ne peuvent être que des string de un seule charactère (pour que l'affichage soit jolie). + """ + Minimax().jouer() # On lance la partie à l'instanciation du Morpion