Complete reorganization

-> `Minimax()` now depends on `Morpion()` and not the other way around
-> Better comments
-> Dynamic display depending on the board
-> Added source for bold text
-> Due to inheritance changes, `Morpion()` is now playable alone 🎉 by calling it without going through `Minimax()`
This commit is contained in:
Mylloon 2021-11-04 20:36:49 +01:00
parent c0037d0941
commit 662e2daf58

View file

@ -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: class Morpion():
"""Définition de l'algorithme minimax.""" """Implémentation du Morpion."""
def __init__(self, terminale: list, humain: str, ordinateur: str) -> None: def __init__(self, joueurA: str = 'X', joueurB: str = 'O', taille: tuple = (3, 3)) -> None:
self.terminale = terminale
self.MIN = humain
self.MAX = ordinateur
def evaluation(self, nbAlign: list) -> list:
""" """
`p` -> position Initalise la classe Morpion :
- Par défaut :
`nbAlign_i(p, J)` -> le nombre d'alignements réalisables, par le joueur `J`, pour lesquels il a déjà `i` symboles posés. - Le joueur A se nomme `X`.
- Le joueur B se nomme `O`.
f(p) = (3.nbAlign_2(p, humain) + nbAlign_1(p, humain)) - (3.nbAlign_2(p, ordinateur) + nbAlign_1(p, ordinateur)) - Le plateau à une taille `3x3`.
""" """
pass if len(joueurA) != 1 or len(joueurB) != 1: # Gestion erreur nom des joueurs
print("Nom des joueurs invalide.")
def terminale(self, etat: list) -> bool: return
"""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]) self.plateau: list = self._definitionPlateau(taille[0], taille[1])
if len(self.plateau) == 0: # Gestion erreur du plateau if len(self.plateau) == 0: # Gestion erreur du plateau
print("Taille du plateau invalide.") print("Taille du plateau invalide.")
return return
self.nbCasesGagnantes = self._recuperationNbCasesGagnantes() self.nbCasesGagnantes = self._recuperationNbCasesGagnantes() # définit combien de cases les joueurs doivent aligner pour gagner
if len(joueurA) != 1 or len(joueurB) != 1: # Gestion erreur nom des joueurs self.joueurA = joueurA # définit le joueur A
print("Nom des joueurs invalide.") self.joueurB = joueurB # définit le joueur B
return self.joueurActuel = choice([joueurA, joueurB]) # le joueur qui commence est choisi aléatoirement car le nom 'X' peut varier
super().__init__(self._recuperationEtatsGagnants(), joueurA, joueurB) self.gagnant = '' # à la fin, montre qui est le gagnant
self.joueurA = joueurA """
self.joueurB = joueurB Permet un bel affichage, la variable `tailleMaxCharactere` augmente en fonction de la taille du tableau et permet
self.gagnant = '' 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: 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: if x <= 0 or y <= 0:
return [] return []
@ -103,24 +63,20 @@ class Morpion(Minimax):
return res 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: def afficher(self) -> None:
"""Affiche le plateau.""" """Affiche le plateau."""
print("\n" * 5) # petit espace par rapport à ce que le terminale de l'utilisateur à afficher dernièrement 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)): 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(f"{espace[1:] if i == 0 else espace}", end = "\n ") # on retire le saut de ligne à la première itération
print("", end = "| ") print("", end = "| ")
for j in range(len(self.plateau[i])): for j in range(len(self.plateau[i])):
n = self.plateau[i][j] n = self.plateau[i][j]
if type(n) == int: 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: 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) print(espace)
def _caseOccupee(self, x: int, y: int) -> bool: def _caseOccupee(self, x: int, y: int) -> bool:
@ -211,9 +167,9 @@ class Morpion(Minimax):
x, y = self._coordonneesCaseDepuisNumero(n) x, y = self._coordonneesCaseDepuisNumero(n)
self.plateau[x][y] = joueur 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.""" """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 = "") print(f"{prefix} Entrez le numéro de case que vous voulez jouer : ", end = "")
reponse = -1 reponse = -1
while int(reponse) < 0: while int(reponse) < 0:
@ -236,19 +192,82 @@ class Morpion(Minimax):
return int(reponse) return int(reponse)
def gras(self, texte: str) -> str: 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" 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: def jouer(self) -> None:
"""Lance la partie de Morpion.""" """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 self.afficher() # affichage du plateau
reponse = self._demandeCase() # on demande où le joueur veut posé sa pièce if self.joueurActuel == self.joueurA:
self._placementPiece(self.joueurA, reponse) # on place la pièce du joueur self._placementPiece(self.joueurA, self._demandeCaseA()) # on place la pièce du joueur là où il veut
# self._placementPiece(self.joueurB, self.algorithme()) 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 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 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