This repository has been archived on 2022-03-31. You can view files and clone it, but cannot push or open issues or pull requests.
morpionMinimax/minimax.py

255 lines
12 KiB
Python
Raw Normal View History

2021-11-04 18:34:19 +01:00
from math import sqrt # Utile pour avoir dynamiquement le nombre de cases a validé pour gagner une partie
2021-11-04 17:03:13 +01:00
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
2021-11-04 18:34:19 +01:00
def algorithme(self, n: int, p: list, j: str) -> list: # p state, n depth, j player
2021-11-04 17:03:13 +01:00
"""É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
2021-11-04 18:34:19 +01:00
self.gagnant = ''
2021-11-04 17:03:13 +01:00
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
2021-11-04 17:03:13 +01:00
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
2021-11-04 17:03:13 +01:00
rechercheLigne = 0
rechercheColonne = 0
# les diagonales vont vers la droite
2021-11-04 17:03:13 +01:00
rechercheDiagonaleVersBas = 0
rechercheDiagonaleVersHaut = 0
joueur = self.plateau[i][j]
while (rechercheLigne != -1) or (rechercheColonne != -1) or (rechercheDiagonaleVersBas != -1) or (rechercheDiagonaleVersHaut != -1):
2021-11-04 17:03:13 +01:00
# -- recherche en ligne --
if len(self.plateau[i]) - j >= self.nbCasesGagnantes: # s'il y a techniquement assez de cases devant pour gagner
2021-11-04 17:03:13 +01:00
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
fini = True
break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while
2021-11-04 17:03:13 +01:00
# -- recherche en colonne --
if len(self.plateau) - i >= self.nbCasesGagnantes: # s'il y a techniquement assez de cases devant pour gagner
2021-11-04 17:03:13 +01:00
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
fini = True
break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while
2021-11-04 17:03:13 +01:00
# -- recherche en diagonale vers le bas --
if (len(self.plateau) - i >= self.nbCasesGagnantes) and (len(self.plateau[i]) - j >= self.nbCasesGagnantes): # s'il y a techniquement assez de cases devant pour gagner
2021-11-04 17:03:13 +01:00
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
fini = True
break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while
2021-11-04 17:03:13 +01:00
# -- recherche en diagonale vers le haut --
if (len(self.plateau) - self.nbCasesGagnantes >= 0) and (len(self.plateau[i]) - j >= self.nbCasesGagnantes): # s'il y a techniquement assez de cases devant pour gagner
idxI = i - rechercheDiagonaleVersHaut # i ne peut pas être <= 0
if idxI >= 0 and self.plateau[idxI][j + rechercheDiagonaleVersHaut] == joueur:
2021-11-04 17:03:13 +01:00
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
fini = True
break # si partie finie ça ne sert a rien de continuer, donc on quitte la boucle while
2021-11-04 17:03:13 +01:00
if fini: # meme chose, on quitte la boucle for (j) si on a fini
break
if fini: # meme chose, on quitte la boucle for (i) si on a fini
2021-11-04 17:03:13 +01:00
break
2021-11-04 18:34:19 +01:00
if fini:
self.gagnant = joueur
2021-11-04 17:03:13 +01:00
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
2021-11-04 17:03:13 +01:00
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"
2021-11-04 17:03:13 +01:00
if len(erreur) > 0:
print(f"{prefix} ❌ Valeur invalide ({erreur}), réessayez : ", end = "")
2021-11-04 17:03:13 +01:00
reponse = -1
return int(reponse)
2021-11-04 18:34:19 +01:00
def gras(self, texte: str) -> str:
"""Fonction qui renvoie le texte en argument en gras."""
return f"\033[1m{texte}\033[0m"
2021-11-04 17:03:13 +01:00
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
2021-11-04 18:34:19 +01:00
# self._placementPiece(self.joueurB, self.algorithme())
2021-11-04 17:03:13 +01:00
self.afficher() # affichage du plateau final
2021-11-04 18:34:19 +01:00
print(f"🎉 Partie terminée, le {self.gras(f'joueur {self.gagnant}')} a gagné !")
2021-11-04 17:03:13 +01:00
if __name__ == "__main__": # Si on lance directement le fichier et on s'en sert pas comme module
2021-11-04 17:26:37 +01:00
Morpion('X', 'O').jouer() # On lance la partie à l'instanciation du Morpion
2021-11-04 18:34:19 +01:00
# Morpion('X', 'O', (4, 4)).jouer() # Si on veut lancer le morpion avec un plateau 4x4