2021-11-04 21:02:37 +01:00
import sys
from math import sqrt
2021-11-04 20:36:49 +01:00
from random import choice
2021-11-04 17:03:13 +01:00
2021-11-04 20:36:49 +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
"""
2021-11-04 20:36:49 +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 ` 3 x3 ` .
2021-11-04 17:03:13 +01:00
"""
2021-11-04 21:48:22 +01:00
self . erreur = False
2021-11-04 20:36:49 +01:00
if len ( joueurA ) != 1 or len ( joueurB ) != 1 : # Gestion erreur nom des joueurs
print ( " Nom des joueurs invalide. " )
2021-11-04 21:48:22 +01:00
self . erreur = True
2021-11-04 20:36:49 +01:00
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. " )
2021-11-04 21:48:22 +01:00
self . erreur = True
2021-11-04 17:03:13 +01:00
return
2021-11-04 20:36:49 +01:00
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 :
"""
2021-11-04 20:36:49 +01:00
Renvoie un plateau en fonction de dimensions ` x ` et ` y ` .
2021-11-04 17:03:13 +01:00
2021-11-04 20:36:49 +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
2021-11-04 20:36:49 +01:00
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 :
2021-11-04 20:36:49 +01:00
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 :
2021-11-04 20:36:49 +01:00
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 )
2021-11-04 17:24:09 +01:00
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 ] ) ) :
2021-11-04 17:24:09 +01:00
if self . _caseOccupee ( i , j ) : # si case occupé par un joueur
2021-11-04 17:03:13 +01:00
rechercheLigne = 0
rechercheColonne = 0
2021-11-04 18:16:32 +01:00
# les diagonales vont vers la droite
2021-11-04 17:03:13 +01:00
rechercheDiagonaleVersBas = 0
rechercheDiagonaleVersHaut = 0
joueur = self . plateau [ i ] [ j ]
2021-11-04 18:16:32 +01:00
while ( rechercheLigne != - 1 ) or ( rechercheColonne != - 1 ) or ( rechercheDiagonaleVersBas != - 1 ) or ( rechercheDiagonaleVersHaut != - 1 ) :
2021-11-04 17:03:13 +01:00
# -- recherche en ligne --
2021-11-04 18:16:32 +01:00
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
2021-11-04 18:16:32 +01:00
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
2021-11-04 18:16:32 +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
2021-11-04 18:16:32 +01:00
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
2021-11-04 18:16:32 +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
2021-11-04 18:16:32 +01:00
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
2021-11-04 18:16:32 +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
2021-11-04 18:16:32 +01:00
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
2021-11-04 18:16:32 +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
2021-11-04 17:24:09 +01:00
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 ] ) )
2021-11-04 21:48:22 +01:00
def _placementPiece ( self , valeur , n : int ) - > None : # valeur est une Union de str et int car il peut être ammené à être modifié avec Minimax
2021-11-04 17:24:09 +01:00
""" Place la pièce d ' un joueur dans le plateau. """
x , y = self . _coordonneesCaseDepuisNumero ( n )
2021-11-04 21:48:22 +01:00
self . plateau [ x ] [ y ] = valeur
2021-11-04 17:24:09 +01:00
2021-11-04 20:36:49 +01:00
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. """
2021-11-04 20:36:49 +01:00
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 "
2021-11-04 17:24:09 +01:00
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 :
2021-11-04 17:24:09 +01:00
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 :
2021-11-04 20:36:49 +01:00
""" 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 "
2021-11-04 20:36:49 +01:00
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. """
2021-11-04 20:36:49 +01:00
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
2021-11-04 20:36:49 +01:00
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
2021-11-05 02:19:45 +01:00
if self . _egalite ( ) and len ( self . gagnant ) == 0 :
2021-11-04 20:36:49 +01:00
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. """
2021-11-04 21:48:22 +01:00
def __init__ ( self , humain : str = ' X ' , ordinateur : str = ' O ' , taille : tuple = ( 3 , 3 ) ) - > None :
2021-11-04 20:36:49 +01:00
"""
Initalise la classe Minimax héritant du Morpion .
- Taille par défaut : ` 3 x3 ` .
- Joueur A est l ' humain.
- Joueur B est l ' ordinateur.
"""
2021-11-04 21:48:22 +01:00
super ( ) . __init__ ( humain , ordinateur , taille = taille )
if self . erreur : # en cas d'erreur dans la classe parent
return
self . humain = self . joueurA
self . robot = self . joueurB
2021-11-04 20:36:49 +01:00
def _demandeCaseB ( self ) - > int :
""" Utilise l ' algorithme `Minimax` pour jouer le coup du joueur B. """
2021-11-04 21:48:22 +01:00
return self . minimax ( self . robot ) [ 1 ]
2021-11-04 20:36:49 +01:00
2021-11-04 21:48:22 +01:00
def _terminerOuEgaliter ( self ) - > bool :
""" Retourne vrai si il y a un gagnant ou si le plateau est plein. """
return not len ( self . gagnant ) == 0 or self . _egalite ( )
def _coupsPossibles ( self ) - > list :
2021-11-04 21:54:22 +01:00
""" Renvoie la liste des coups possibles dans le plateau. """
2021-11-04 21:48:22 +01:00
coups = [ ]
for i in range ( len ( self . plateau ) ) :
for j in range ( len ( self . plateau [ i ] ) ) :
if not self . _caseOccupee ( i , j ) :
coups . append ( self . plateau [ i ] [ j ] )
return coups
2021-11-04 21:54:22 +01:00
def _ennemi ( self , joueur ) - > str :
""" Renvoie l ' ennemi du joueur en argument. """
2021-11-04 21:48:22 +01:00
if joueur == self . humain :
return self . robot
return self . humain
2021-11-04 21:54:22 +01:00
def minimax ( self , joueur : str , profondeur : int = 0 ) :
"""
Fonction Minimax qui décide quel case est la plus intéressante .
Profondeur par défaut à ` 0 ` car commence à ` 0 ` .
"""
2021-11-04 21:48:22 +01:00
if joueur == self . robot :
2021-11-04 22:23:34 +01:00
meilleursCas = - 1
2021-11-04 21:48:22 +01:00
else :
2021-11-04 22:23:34 +01:00
meilleursCas = 1
2021-11-04 21:48:22 +01:00
if self . _terminerOuEgaliter ( ) :
2021-11-04 22:23:34 +01:00
if self . gagnant == self . humain :
return - 1 + profondeur , None
2021-11-04 21:48:22 +01:00
elif self . _egalite ( ) :
return 0 , None
elif self . gagnant == self . robot :
2021-11-04 22:23:34 +01:00
return 1 - profondeur , None
2021-11-04 21:48:22 +01:00
for numero in self . _coupsPossibles ( ) :
2021-11-04 22:23:34 +01:00
self . _placementPiece ( joueur , numero ) # on essaie de jouer un coup
valeurEvaluation , _ = self . minimax ( self . _ennemi ( joueur ) , profondeur + 1 ) # on voit ce que ça donne avec le coup que l'on vient de mettre
self . _placementPiece ( numero , numero ) # on replace l'ancienne valeur
2021-11-04 21:48:22 +01:00
if joueur == self . robot :
if valeurEvaluation > meilleursCas :
meilleursCas , meilleurNumero = valeurEvaluation , numero
else :
if valeurEvaluation < meilleursCas :
meilleursCas , meilleurNumero = valeurEvaluation , numero
return meilleursCas , meilleurNumero
2021-11-04 17:03:13 +01:00
2021-11-04 18:16:32 +01:00
if __name__ == " __main__ " : # Si on lance directement le fichier et on s'en sert pas comme module
2021-11-04 20:36:49 +01:00
"""
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 4 x6 ) .
- > 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 20:36:49 +01:00
"""
2021-11-04 21:54:22 +01:00
sys . argv . pop ( 0 ) # on retire le nom du fichier (première valeure de `argv`)
2021-11-04 21:02:37 +01:00
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 :
2021-11-04 21:48:22 +01:00
if not " list index out of range " in str ( e ) :
print ( f " Un argument n ' a pas été compris ( { e } )... Lancement du Morpion avec les paramètres par défaut. " )
2021-11-04 21:02:37 +01:00
Minimax ( ) . jouer ( ) # On lance la partie à l'instanciation du Morpion