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:
"""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