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

291 lines
16 KiB
Python
Raw Normal View History

2021-11-04 21:02:37 +01:00
import sys
from math import sqrt
from random import choice
2021-11-04 17:03:13 +01:00
class Morpion():
"""Implémentation du Morpion."""
def __init__(self, joueurA: str = 'X', joueurB: str = 'O', taille: tuple = (3, 3)) -> None:
2021-11-04 17:03:13 +01:00
"""
Initalise la classe Morpion :
- Par défaut :
- Le joueur A se nomme `X`.
- Le joueur B se nomme `O`.
- Le plateau à une taille `3x3`.
2021-11-04 17:03:13 +01:00
"""
if len(joueurA) != 1 or len(joueurB) != 1: # Gestion erreur nom des joueurs
print("Nom des joueurs invalide.")
return
2021-11-04 17:03:13 +01:00
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() # 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
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`.
2021-11-04 17:03:13 +01:00
Les cellules vides sont correspondent à leur "numéro de case".
2021-11-04 17:03:13 +01:00
"""
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 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]) * (3 + self.tailleMaxCharactere)}-" # taille des lignes qui séparent les valeurs du plateau
2021-11-04 17:03:13 +01:00
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:0{self.tailleMaxCharactere}d}", end = " | ") # on rajoute un "0" au chiffre < 10 (comme en C avec `%02d`)
2021-11-04 17:03:13 +01:00
else:
nombreEspaceRequis = (self.tailleMaxCharactere - 1) // 2
print(f"{' ' * nombreEspaceRequis}{n}{' ' * nombreEspaceRequis}", end = " | ") # espace automatique pour pas décaler l'affichage
2021-11-04 17:03:13 +01:00
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
def _demandeCase(self, joueur) -> int:
2021-11-04 17:03:13 +01:00
"""Demande au joueur sur quelle case il veut poser sa pièce."""
prefix = f"({joueur} - {self.nbCasesGagnantes} cases successives pour gagner)"
2021-11-04 17:03:13 +01:00
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.""" # source: https://stackoverflow.com/a/17303428
2021-11-04 18:34:19 +01:00
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
2021-11-04 17:03:13 +01:00
def jouer(self) -> None:
"""Lance la partie de Morpion."""
while not self._terminer() and not self._egalite(): # tant que la partie n'est pas terminé
2021-11-04 17:03:13 +01:00
self.afficher() # affichage du plateau
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
2021-11-04 17:03:13 +01:00
self.afficher() # affichage du plateau final
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()
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
"""
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).
2021-11-04 21:02:37 +01:00
-> le fichier s'adapte aux arguments données :
-> Si un argument : Précisez la taille du tableau, exemple : "3x3".
-> Si deux arguments : Précisez le nom des deux joueurs qui vont jouer (Rappel: un joueur = un charactère).
-> Si trois arguments : Précisez alors la taille le nom des deux joueurs et la taille du tableau, en suivant les règles précédentes.
"""
2021-11-04 21:02:37 +01:00
sys.argv.pop(0)
try: # on ne vérifie pas si la taille est bonne, en cas d'erreur on lance le programme avec les paramètres par défaut
if len(sys.argv) == 1:
Minimax(taille = tuple([int(i) for i in sys.argv[0].split('x')])).jouer() # On spécifie la taille du tableau
elif len(sys.argv) == 2:
Minimax(*sys.argv).jouer() # On spécifie les joueurs
else:
Minimax(sys.argv[0], sys.argv[1], tuple([int(i) for i in sys.argv[2].split('x')])).jouer() # On spécifie les joueurs et la taille du tableau
except Exception as e:
print(f"Un argument n'a pas été compris ({e})... Lancement du Morpion avec les paramètres par défaut.")
Minimax().jouer() # On lance la partie à l'instanciation du Morpion