Le jeu du motus en python

Projets

MOTUS est à l’origine un jeu télévisé diffusé entre 1990 et 2019 sur France 2. Ce programme d’origine américaine s’inspire du jeu MasterMind, les lettres ayant remplacés les chiffres. Ce jeu a connu depuis de nombreuses variantes dont le célèbre WORDLE qui connait un très grand succès actuellement sur internet.

Introduction

Les règles du jeu sont plutôt simples : l’objectif est de trouver un mot d’un nombre fixé de lettres en au plus 6 essais. La première lettre du mot à découvrir est donnée et après chaque tentative :

  • les lettres communes et bien placées sont signalées par une case rouge
  • les lettres communes mais mal placées sont, quant à elles, signalées par une case jaune

Si le joueur parvient à découvrir le mot avant les 6 essais, il remporte la partie, sinon ce mot lui est dévoilé.

Certaines versions du jeu, dont WORDLE, se jouent à une personne, même si à l’origine le jeu télévisé se joue à deux. Dans la version à 2 joueurs, aux règles précédentes s’ajoutent les suivantes : un joueur perd la main si le mot proposé :

  • n’est pas de la bonne longueur
  • ne commence pas par la lettre proposée
  • est mal épelé
  • n’est pas donné dans les 8 secondes imparties
  • ne figure pas dans le dictionnaire
  • n’a pas été trouvé au terme des 6 essais

Dans chacun de ces cas, le joueur adverse récupère la main et bénéficie d’une lettre bonus : la première lettre non découverte lui est dévoilée. Il dispose alors du nombre d’essais restants (du joueur précédent) pour trouver le mot. Dans le cas ou il ne dispose que d’une tentative pour découvrir le mot, il conserve la main pour la grille suivante même en cas d’échec.

Notre projet est donc de programmer en langage python ce jeu. D’une part parce que nous aimons bien jouer à WORDLE (nous avons découvert le jeu original en visionnant une partie sur Youtube) et d’autre part car la programmation de ce jeu nous semblait accessible (pour notre niveau) sans être trop simple et offrait de nombreuses possibilités d’évolutions et améliorations (mode 2 joueurs, prise en compte de règles supplémentaires, interface graphique).

La première question qui s’est naturellement posée dès le départ concernait le choix de l’interface : en mode console ou plutôt avec une interface graphique ? Après une mure réflexion et avoir consulté plusieurs sites notamment de NSI, nous nous sommes rendus à l’évidence : une interface graphique nécessitait un passage obligé par le module tkinter particulièrement adapté à ce jeu. Et même si ce module semble accessible pour les novices que nous sommes, nous avons jugé plus prudent dans un premier temps, de travailler sur un jeu en mode console.

Après avoir réfléchi à la structure de notre programme, nous avons écrit ensemble les différentes fonctions que devra utiliser le jeu. Puis, comme il nous restait du temps, nous avons décidé de programmer à partir de ces fonctions, deux modes de jeu : un mode 1 joueur et un mode à 2. Nous nous sommes répartis le travail et chacun de nous a développé son propre mode et nous les avons assemblés dans le programme final.

Notre programme

Nous avons essayé de rester les plus fidèles au jeu original même si comme nous le verrons plus tard, nous n’avons pas implémenté toutes les règles présentées précedemment.

Concernant l’affichage, avant que le joueur ne fasse sa proposition, nous affichons en majuscules les lettres (bien placées) qui ont été découvertes jusque là (la première lettre étant donnée dès le départ), les lettres manquantes étant remplacées par un tiret. Puis après la saisie du mot, nous affichons en majuscule les lettres communes avec les mots à découvrir bien placées et en minuscule les lettres communes mal placées.

La partie : notre jeu offre la possibilité de jouer à un ou à deux . Après avoir sélectionner le nombre de joueur, la longueur du mot à deviner est demandé (entre 5 et 10 lettres). Puis dans le cas d’une partie à 2, le nombre de grilles de jeu sur lesquelles les adversaires souhaitent s’affronter est demandé. Dans le mode solo, chaque partie s’effectue sur une seule grille.

Les règles : dans la version 1 joueur pour qu’un essai soit validé, le mot proposé doit figurer dans le dictionnaire (et doit être bien orthographié), il doit commencer par la bonne lettre et doit être de la bonne longueur. Si le mot n’est pas valide, il est redemandé au joueur sans qu’il ne soit pénalisé par la perte d’un essai. Voici un exemple de partie :

Dans la version 2 joueurs, la main passe

  • dans chacun des cas précédents
  • au cas ou le mot n’est pas découvert au terme des 6 essais, auquel cas l’adversaire bénéficie d’une seule tentative pour découvrir le mot, mais conserve la main pour la grille suivante en cas d’échec.

Les fonctions

la fonction dico_n_lettres

def dico_n_lettres(dico):
#fonction qui crée une fois pour toute des dictionnaires
#de mots avec un nombre donné de lettres à partir d un
#fichier texte
   for n in range(1,25):
      path=os.getcwd()+'/'                   # définit le répertoire ou se trouve le ditionnaire
      dico1 = path+dico+".txt"               # fichier contenant le dictionnaire 
      dico2 = path+dico+str(n)+".txt"        # fichier qui contiendra le dictionnaire des mots à n lettres 
      fichier1 = open(dico1, "r")
      fichier2 = open(dico2, "w")
      for mot in fichier1:
          if len(mot) == n+1:                # n+1 car on doit tenir compte du caractère de retour à la ligne
              fichier2.write(mot)     
      fichier1.close()
      fichier2.close()

Nous avons récupéré sur le site https://www.lama.univsavoie.fr/pagesmembres/hyvernat/Enseignement/1415/info113/tp6.html deux dictionnaires sous la forme de fichiers .txt . Le premier contrairement au second contient les formes conjuguées des verbes et les pluriels des noms. La fonction précédente a pour but de créer une seule fois (ils sont installés sur le répertoire courant lors de la première exécution du programme), à partir du dictionnaire choisi, des dictionnaires constitués uniquement des mots de même longueur.

La fonction choisir_un_mot

def choisir_un_mot(dico,n):
#fonction qui va choisir aléatoirement un mot
#de n lettres
    path=os.getcwd()+'/'
    dico = path+dico+str(n)+".txt"
    fichier = open(dico, "r")
    liste_mots = fichier.readlines() # met tous les mots du fichiers dans une liste
    i = randint(1,len(liste_mots))
    mot = liste_mots[i]              # prend au hasard un mot dans la liste
    mot = mot.replace('\n','')       # supprime le caractère de retour à la ligne
    fichier.close()
    return(mot)

Cette fonction va utiliser le dictionnaire créé par la fonction précédente pour sélectionner aléatoirement un mot de n lettres qui servira de mot à deviner dans le jeu.

La fonction lettres_placées

def lettres_placees(liste1,liste2,liste3):
# fonction qui met dans liste1 les lettres 
# communes bien placées des liste2 et liste3
    liste = []
    liste0 = []
    for i in range(len(liste2)):
        if liste2[i]==liste3[i]:
            liste0.append(liste2[i])
        else:
            liste0.append('')
    for i in range(len(liste1)):
        if liste1[i]==liste0[i]:
            liste.append(liste1[i])
        else:
            liste.append(liste1[i]+liste0[i])
    return(liste)

Avec la fonction lettres_communes elles sont le cœur de notre programme. Cette fonction a pour but de mettre à jour la liste des lettres bien placées découvertes depuis le début de la partie. La variable liste1 contient la liste des lettres bien placées à mettre à jour et liste2 et liste3 les listes à comparer (le mot mystère et le dernier mot joué) et dont les lettres communes et bien placées vont être ajoutées à liste1.

La fonction lettres_communes

def lettres_communes(liste1,liste2):
# Retourne une liste avec toutes les lettres
# de la liste1 se trouvant dans la liste2
# les lettres bien placées sont en majuscule
# les autres sont en minuscule.
    liste = []
    liste3 = []
    for i in range(len(liste1)):
       liste.append('')
       liste3.append(liste2[i])
    for i in range(len(liste1)):
        if liste1[i]==liste3[i]:
            liste[i]=liste1[i].upper()
            liste3[i]=''
    for i in range(len(liste1)):
        for j in range(len(liste1)):
            if liste1[i]==liste3[j] and liste[i]=='':
                liste[i]=liste1[i].lower()
                liste3[j]=''
                break     
    return(liste)

Cette fonction est chargée de trouver les lettres communes entre liste1 et liste2, (qui représentent le mot joué et le mot à deviner) retournant ces lettres en majuscule pour les lettres bien placées et en minuscules pour les autres. Une des principales difficulté dans la conception de cette fonction a été la gestion des lettres dont le nombre d’apparition était différente dans les deux mots.

La fonction teste_mot

def teste_mot(liste,lettre,dico,n):
# Teste si le mot proposé par le joueur
# est valide : il doit commencer par 'lettre'
# se trouver dans dico et être de n lettres.
    test=True
    mot=''.join(liste).lower()   #transforme 'liste' en chaine de caractère en minuscule
    path=os.getcwd()+'/'
    dico = path+dico+str(n)+".txt"
    fichier = open(dico, "r")
    liste1 = fichier.readlines() # met tous les mots du dico dans une liste
    liste_mots=[]
    for mot1 in liste1:
        liste_mots.append(mot1.replace('\n',''))
    if  len(liste)!=n:                # teste si le mot est de la bonne longueur
       test=False
       print('Ce mot n est pas de la bonne longueur.')
    elif (mot not in liste_mots):     # teste si le mot est dans le dico
       test=False
       print('Ce mot n est pas dans le dictionnaire.')
    elif liste[0]!=lettre:            # teste si le mot commence par la bonne lettre
       test=False            
       print('Ce mot ne commence pas par la lettre '+lettre)
    fichier.close()
    return(test)

Cette fonction a pour but la validation (ou pas) de chaque mot entré par le joueur. Le mot est validé s’il commence par la bonne lettre, est constitué du bon nombre de lettres et se trouve dans le dictionnaire.

Les difficultés rencontrées

1. Pour la création et la gestion des dictionnaires nous nous sommes aidés du site https://python.sdv.univ-paris-diderot.fr/07_fichiers/ qui explique clairement les manipulations de base des fichiers (ouverture en mode lecture/écriture, fermeture, mise sous forme de liste de chaîne de caractères du contenu,…) et nous avons dû nous familiariser avec le module os pour indiquer le répertoire dans lequel se trouve les dictionnaires. Une des difficulté qui est apparue lors de la création des dictionnaires vient du fait que la longueur des mots ne correspondait pas à celle demandée (par exemple les mots de 5 lettres apparaissaient lorsqu’on en demandait 6. C’est après plusieurs tests que nous avons compris que le décalage venait du caractère ‘saut de ligne’ présent mais qui n’apparaissait pas quand on affichait les mots.

2. Pour la fonction « lettres_communes » la gestion des lettres dont le nombre d’apparition était différente entre les mots à comparer nous a aussi posé quelques problèmes. Pour y remédier, la solution que nous avons trouvé a été d’ « effacer » du mot à deviner une lettre commune une fois qu’elle a été trouvée (pour éviter de la recompter si elle apparaît une nouvelle fois dans le mot joué), en remplaçant la lettre en question par le caractère ‘ ‘. Alors que cette opération d’effacement s’effectuait en local (uniquement à l’intérieur de la fonction), la liste modifiée l’était également dans le programme principal. Pour régler le problème, nous avons décidé d’utiliser une liste intermédiaire, pour éviter de toucher à la liste passée en argument. Mais le résultat fut le même. Après quelques recherches sur internet, nous avons compris que le problème était inhérent aux listes et après plusieurs tentatives , nous sommes parvenus à régler le problème en passant par une liste auxiliaire créée non plus par une simple affectation mais en la remplissant grâce à une boucle et la commande .append().

3. Nous avons tenté de prendre en compte la gestion du temps, c’est-a-dire limiter à 10 s la durée de chaque essai mais la simple utilisation du module time ne donnait pas de résultat satisfaisant. En cherchant sur internet, nous avons vu qu’il était possible de créer un minuteur grâce au module threading mais après quelques essais cela s’est avéré plus compliqué que prévu et nous avons décidé de renoncer à cette fonctionnalité pour cette version mais nous espérons et envisageons de l’ajouter dans le futur.

Le fichier motus.py et les dictionnaires

Conclusion

Ce projet libre nous a permis de réaliser notre premier jeu en python. Après avoir longuement hésité, nous avons finalement opté pour MOTUS qui s’est finalement révélé très intéressant à programmer avec quelques challenges. Bien sûr, il reste encore beaucoup à accomplir pour se rapprocher du jeu original, notamment l’utilisation d’une interface graphique grâce au module tkinter ou encore la gestion du temps. Nous avions également pensé à intégrer des niveaux de difficulté en créant par exemple des dictionnaires adaptés (en associant à chaque mot un coefficient indiquant sa « difficulté » sur le même principe que le Scrabble). Toutes ces améliorations feront peut-être l’objet d’un futur projet.