diff --git a/minimax.py b/minimax.py new file mode 100644 index 0000000..da6644e --- /dev/null +++ b/minimax.py @@ -0,0 +1,232 @@ +from math import sqrt + +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 main(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 + + 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 _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 type(self.plateau[i][j]) == str: # si case occupé par un joueur + rechercheLigne = 0 + rechercheColonne = 0 + rechercheDiagonaleVersBas = 0 + rechercheDiagonaleVersHaut = 0 + joueur = self.plateau[i][j] + while (rechercheLigne != -1) or (rechercheColonne != -1) or (rechercheDiagonaleVersHaut != -1) or (rechercheDiagonaleVersBas != -1): + # -- recherche en ligne -- + if len(self.plateau[i]) - j - rechercheLigne >= self.nbCasesGagnantes - rechercheLigne: # 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 + rechercheLigne = -1 + fini = True + + # -- recherche colonne -- + if len(self.plateau) - i - rechercheColonne >= self.nbCasesGagnantes - rechercheColonne: # 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 + rechercheColonne = -1 + fini = True + + # -- recherche diagonale vers le bas -- + if (len(self.plateau) - i - rechercheDiagonaleVersBas >= self.nbCasesGagnantes - rechercheDiagonaleVersBas) and (len(self.plateau[i]) - j - rechercheDiagonaleVersBas >= self.nbCasesGagnantes - rechercheDiagonaleVersBas): # 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 + rechercheDiagonaleVersBas = -1 + fini = True + + # -- recherche diagonale vers le haut -- + if (len(self.plateau) >= self.nbCasesGagnantes) and (len(self.plateau[i]) - j - rechercheDiagonaleVersHaut >= self.nbCasesGagnantes - rechercheDiagonaleVersHaut): # s'il y a techniquement assez de cases devant pour gagner + if self.plateau[i - rechercheDiagonaleVersHaut][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 + rechercheDiagonaleVersHaut = -1 + fini = True + + if fini: # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle + break + if fini: # meme chose, on quitte la boucle si on a fini + break + + return fini + + 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" + if len(erreur) > 0: + print(f"{prefix} Valeur invalide ({erreur}), réessayez : ", end = "") + reponse = -1 + + return int(reponse) + + def _placementPiece(self, joueur: str, n: int) -> None: + """Place la pièce d'un joueur dans le plateau.""" + x = (n - 1) // len(self.plateau[0]) + y = (n - 1) % len(self.plateau[0]) + self.plateau[x][y] = joueur + + 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.afficher() # affichage du plateau final + print("Partie terminé, un joueur a gagné !") + +if __name__ == '__main__': # Si on lance directement le fichier et on s'en sert pas comme module + Morpion('X', 'O', (6, 4)).jouer() # On lance la partie à l'instanciation du Morpion