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:
parent
c0037d0941
commit
662e2daf58
1 changed files with 103 additions and 84 deletions
187
minimax.py
187
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:
|
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
|
||||||
|
|
Reference in a new issue