243 lines
12 KiB
Python
243 lines
12 KiB
Python
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 _caseOccupee(self, x: int, y: int) -> bool:
|
|
"""Vrai si la case en `x` et `y` est déjà occupée par un joueur."""
|
|
return type(self.plateau[x][y]) == str
|
|
|
|
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 self._caseOccupee(i, j): # 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 _coordonneesCaseDepuisNumero(self, numero: int) -> tuple[int, int]:
|
|
"""Renvoie les coordonnées d'une case."""
|
|
return ((numero - 1) // len(self.plateau[0]), (numero - 1) % len(self.plateau[0]))
|
|
|
|
def _placementPiece(self, joueur: str, n: int) -> None:
|
|
"""Place la pièce d'un joueur dans le plateau."""
|
|
x, y = self._coordonneesCaseDepuisNumero(n)
|
|
self.plateau[x][y] = joueur
|
|
|
|
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"
|
|
else:
|
|
x, y = self._coordonneesCaseDepuisNumero(int(reponse))
|
|
if self._caseOccupee(x, y):
|
|
erreur = "case déjà occupée"
|
|
if len(erreur) > 0:
|
|
print(f"{prefix} ❌ Valeur invalide ({erreur}), réessayez : ", end = "")
|
|
reponse = -1
|
|
|
|
return int(reponse)
|
|
|
|
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
|