# Auteur : Jérome Charrière
# Date : Juin 2013
# Version : 2.0
# Description:

#  Ce programme réalise un jeu du pendu avec interface graphique. Le jeu
#  choisit aléatoirement un mot situé dans un fichier texte et le
#  mémorise. L'utilisateur doit essayer de deviner le mot à l'aide de
#  plusieurs essais successifs. Pour chaque lettre proposée à l'aide
#  d'un clavier virtuel, l'application vérifie si la lettre se situe
#  dans le mot secret et donne à l'utilisateur un feed-back en affichant
#  l'état du pendu et de la découverte du mot secret.

from tkinter import *
from random import *
from datetime import date

### Classe principale de l'application ###
#----------------------------------------#

class Application(Tk):

    def __init__(self):
        Tk.__init__(self)
        self.title("Jeu du Pendu")
        self.configure(bg="#2C2F10")

        # pseudo du joueur:
        self.pseudo = ""

        # Création de la zone de texte présentant le jeu:
        Label(self, text="""Entrez une lettre grâce au clavier ci-dessous.\n
Si elle se trouve dans le mot secret, elle sera affichée.\n
Sinon, la sentence se rapprochera...""",
              font=("Comic sans Ms", 14), justify=CENTER, bg='#2C2F10',
              fg="#F0E39E").grid(row = 1, column = 1)

        # Création du clavier:
        self.clavier = Clavier(self, command = self.proposer)
        self.clavier.grid(row = 2, column = 1, padx=5, pady=5)

        # Création de la zone de texte recevant le mot caché:
        self.etatMot = Label(self, text="Appuyez sur l'image pour commencer",
                             font=("Comic sans Ms", 14), fg='red', bg="#cbd888")
        self.etatMot.grid(row=3, column = 1, pady=5)

        # Création du Canevas contenant l'image:
        self.can = Canvas(self, width=250, height=350, bg='#2C2F10', bd=0)
        self.can.grid(row = 1, column = 2, rowspan=3, columnspan=3, padx=10, pady=10)
        self.can.bind("<Button-1>", self.start_it)

        # Affichage de la photo initiale:
        self.photo = PhotoImage(file='annexes/images/intro.gif')
        self.imagePendu = self.can.create_image(125, 175, image=self.photo)

        # Création des boutons:
        Button(self, text="Login", bg="#A65400", fg="white", activebackground="#BF7830",
               activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
               command=self.login).grid(row=4, column=2, pady=5)

        Button(self, text="Scores", bg="#A65400", fg="white", activebackground="#BF7830",
               activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
               command=self.score).grid(row=4, column=3, pady=5)

        Button(self, text="Quitter", bg="#A65400", fg="white", activebackground="#BF7830",
               activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
               command=self.quit).grid(row=4, column=4, pady=5)


        # Création du titre du jeu:
        self.titre = self.can.create_text(125, 20, text="Jeu du Pendu",
                                          font=("Comic sans Ms", 20, 'bold'), fill="red")

        # Création de la zone de texte affichant des messages à l'utilisateur
        self.message = self.can.create_text(125, 310, font=("Comic sans Ms", 12, 'bold'), justify = CENTER,
                                       fill='white')

        # Interrupteur de partie:
        self.jeuTermine = True

    ##################################################
    ### Méthodes nécessaires au déroulement du jeu ###
    ##################################################

    def choisirMot(self, nomFichier):
        """
        choisirMot(str nomFichier) --> str.
        Retourne de manière aléatoire un mot situé dans le fichier <nomFichier>
        après lui avoir enlevé ses accents et l'avoir mis en majuscules
        """

        fichier = open(nomFichier, 'r', encoding='utf8')

        listeMots = fichier.readlines()

        fichier.close()

        index = randint(0, len(listeMots)-1)

        mot = listeMots[index][:-1]     # suppression du caractère de fin de ligne

        # retourne le mot choisi en majuscule et exempt d'accent:
        return (self.enleveAccent(mot).upper()).strip()

    def enleveAccent(self, chaine):
        """
        enleve_accent(str chaine) --> str.
        Enlève les accents d'une chaine de caractères
        """
        result = ""
        for c in chaine:
            if c == 'é' or c == 'è' or c == 'ê' or c == 'ë':
                c = 'e'
            elif c == 'à' or c == 'ä' or c == 'â':
                c = 'a'
            elif c == 'ù' or c == 'û':
                c = 'u'
            elif c == 'ô' or c == 'ò':
                c = 'o'
            elif c == 'î' or c == 'ï':
                c = 'i'
            elif c == 'ç':
                c = 'c'
            result += c
        return result
    

    def affichePendu(self, lettresIncorrectes, lettresCorrectes, motSecret):
        """
        affichePendu(list lettresIncorrectes, list lettresCorrectes, str motSecret) --> None
        Affiche l'image du pendu dont le numéro correspond à la taille de la liste <lettresIncorrectes>
        ainsi que l'état de la recherche du mot secret à partir de la liste <lettresCorrectes>
        """
    
        # Afficher l'état du pendu (élément self.imagePendu):
        i = len(lettresIncorrectes)
        self.photo = PhotoImage(file='annexes/images/pendu_{}.gif'.format(i))
        self.can.itemconfigure(self.imagePendu, image=self.photo)

        # Afficher l'état du mot secret (Label self.etatMot):
        espaces = '_ ' * len(motSecret)

        for i in range(len(motSecret)): # remplace les espaces du mot secret par les lettres manquantes correctes
            if motSecret[i] in lettresCorrectes:
                espaces = espaces[:2*i] + motSecret[i] + espaces[2*i+1:]

        self.etatMot.configure(text=espaces)

        # Effacer l'éventuel message adressé à l'utilisateur (élément dans self.message)
        self.can.itemconfigure(self.message, text="")

    def proposer(self, lettre):
        """
        proposer(str lettre) --> None
        Vérifie si la lettre proposée est située dans le mot secret ou non,
        agit en conséquence et retourne un feedback à l'utilisateur
        """

        if not self.jeuTermine:     # empeche de jouer une fois le jeu terminé ou non commencé

            # si le joueur choisit une lettre déjà proposée:
            if lettre in self.lettresIncorrectes+self.lettresCorrectes:
                self.can.itemconfigure(self.message,
                                       text="Suggestion déjà faite ! \n Choisissez une autre lettre")
            # si le joueur choisit une lettre pas encore proposée:
            else:

                # si la proposition est correcte:
                if lettre in self.motSecret:
                    self.lettresCorrectes.append(lettre)

                    # controler si le joueur a gagné:
                    tousTrouve = True
                    for i in range(len(self.motSecret)):
                        if self.motSecret[i] not in self.lettresCorrectes:
                            tousTrouve = False
                            break

                    if tousTrouve:
                        self.affichePendu(self.lettresIncorrectes, self.lettresCorrectes, self.motSecret)
                        totalEssais = len(self.lettresIncorrectes+self.lettresCorrectes)
                        essaisGagnants = len(self.lettresCorrectes)
                        points = essaisGagnants*100//totalEssais
                        self.sauvegarder(self.pseudo, self.motSecret, points)
                        self.can.itemconfigure(self.message, text="Gagné ! \n Le mot était '{mot}' \n Votre score est de {resultat}".format(mot = self.motSecret, resultat=points))
                        self.jeuTermine = True

                # si la proposition est incorrecte:
                else:
                    self.lettresIncorrectes.append(lettre)

                    # controler si le joueur a perdu (9 erreurs sont autorisées au maximum):
                    if len(self.lettresIncorrectes) == 9:
                        self.affichePendu(self.lettresIncorrectes, self.lettresCorrectes, self.motSecret)
                        self.can.itemconfigure(self.message, text="Perdu ! \n Le mot était '{mot}'.".format(mot = self.motSecret))
                        self.jeuTermine = True

                if self.jeuTermine:
                    self.etatMot.configure(text="Appuyez sur l'image pour rejouer !")

                else:
                    self.affichePendu(self.lettresIncorrectes, self.lettresCorrectes, self.motSecret)

        
    def sauvegarder(self, pseudo, motCherche, points):
        """
        sauvegarder(str pseudo, str motCherche, int points) --> None
        Enregistre sur une seule ligne du fichier associé à pseudo le nombre de <points>
        réalisé lors de la recherche du <motCherche> au format suivant:

        date(4/6/2013):motCherche@points

        Si pseudo est une chaine vide, la sauvegarde n'est pas faite.
        """

        nomFichier = "annexes/joueurs/"+pseudo+'.txt'
        try:
            fichier = open(nomFichier, 'a', encoding='utf8')
            
        except:
            pass
        
        else:

            today = date.today()        # récupération de la date d'aujourd'hui

            ligne = '{date}:{mot}@{pts}\n'.\
                    format(date=str(today.day)+'/'+str(today.month)+'/'+str(today.year),\
                           mot = motCherche,\
                           pts = points)

            fichier.write(ligne)

            fichier.close()


    ###########################################
    ### Méthodes gestionnaires d'événements ###
    ###########################################

    def start_it(self, event):
        """
        start_it(self, event) --> None
        Si le jeu est terminé, initialise les attributs nécessaires
        à une nouvelle partie
        """

        if self.jeuTermine:

            # listes des lettres proposées
            self.lettresCorrectes = []
            self.lettresIncorrectes = []

            # choix du mot secret:
            self.motSecret = self.choisirMot('annexes/mots.txt')

            # état du jeu:
            self.jeuTermine = False

            # lancement du jeu:
            self.affichePendu(self.lettresIncorrectes, self.lettresCorrectes,
                              self.motSecret)

            # Ligne de test permettant de visualiser le mot choisi dans la console:
            print(self.motSecret)

                    
    def login(self):
        "Instancie une fenetre d'inscription"
        Inscription(self, bg = "#2C2F10")

    def score(self):
        "Instancie une fenetre avec les 3 meilleurs scores"
        BestScore(self, bg = "#2C2F10")


### Classes subordonnées de l'application ###
#-------------------------------------------#

class Clavier(Frame):
    "Clavier d'entrée des lettres"

    def __init__(self, boss, command=None):
        
        Frame.__init__(self, boss, bd=1, bg="#cbd888")

        # clavier centré au milieu du cadre:
        i = 0
        lettre = "A"
        colonne = [0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,2,3,4,5,6,7]
        while i < 26:
            ln = 1 + (i//10)
            col = colonne[i]
            Button(self, text=lettre, bg="#A65400", fg="white", activebackground="#BF7830",
                   activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
                   command=lambda arg=lettre: command(arg)).grid(row=ln, column=col, pady=3, padx=2)
            lettre = chr(ord(lettre) + 1)
            i += 1
            

class Inscription(Toplevel):
    "Fenetre satellite permettant à l'utilisateur de s'inscrire"
    def __init__(self, boss, **Arguments):
        Toplevel.__init__(self, boss, **Arguments)
        self.title("Inscription")
        self.master = boss

        # Texte d'invite à entrer un pseudo:
        Label(self, text="Entrez votre pseudo", font=("Comic sans MS", 14), justify=CENTER,
              bg='#2C2F10', fg='#F0E39E').grid(row=1, column = 1, columnspan = 2, padx=5)

        # Champs de saisie de texte permettant d'entrer un pseudo:
        self.saisie = Entry(self, width = 25)
        self.saisie.grid(row=2, column=1, columnspan = 2, padx = 5, pady = 5)

        # Boutons de commande:
        Button(self, text="OK", bg="#A65400", fg="white", activebackground="#BF7830",
               activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
               command = self.creerSauvegarde).grid(row=3, column=1, pady=5)
        Button(self, text="Annuler", bg="#A65400", fg="white", activebackground="#BF7830",
               activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
               command = self.destroy).grid(row=3, column=2, pady=5)

    def creerSauvegarde(self):
        """
        creerSauvegarde(str pseudo) --> None
        Crée, s'il n'existe pas, un fichier nommé 'pseudo.txt' dans le sous-dossier 'joueurs' du répertoire 'annexes'
        et y inscrit en première ligne le titre "Résultats de pseudo".
        """

        pseudo = self.saisie.get()      # récupère le nom de l'utilisateur
        if pseudo != "":

            nomFichier = "annexes/joueurs/"+pseudo+'.txt'

            try:        # tente l'ouverture en mode lecture afin de savoir si le fichier existe
                fichier = open(nomFichier, 'r', encoding='utf8')
                
            except:     # crée le fichier s'il n'existe pas
                fichier = open(nomFichier, 'w', encoding='utf8')
                fichier.write("Résultats de "+ pseudo + ":\n")
                fichier.close()
                
            else:
                fichier.close()

            # retourne le nom de l'utilisateur à l'application principale:
            self.master.pseudo = pseudo

            # ferme la fenetre satellite:
            self.destroy()


class BestScore(Toplevel):
    "Fenetre satellite permettant de visualiser les meilleurs scores"

    def __init__(self, boss, **Arguments):
        Toplevel.__init__(self, boss, **Arguments)
        self.title("Vos meilleurs scores")
        
        if boss.pseudo != "":
            Label(self, text="Meilleurs scores de {}".format(boss.pseudo), font=("Comic sans Ms", 14, 'bold'),
                  justify = CENTER, bg='#F0E39E', fg='#2C2F10').grid(row=1, column=1, padx=5, pady=5)

            scores = self.trierScores(boss.pseudo)      # récupération des scores triés

            # Affichage des 3 meilleurs scores dans la fenetre
            i = 0
            while i < len(scores) and i < 3:
                Label(self, text="({position}) {point} pts pour '{mot}' ({date})".format(position=i+1, point=scores[i][0], mot=scores[i][1], date=scores[i][2]),
                      font = ("Comic sans Ms", 12), bg='#2C2F10', fg='#F0E39E').grid(row = 2+i, column = 1, sticky = 'W')
                i += 1

            # Bouton de destruction de la fenetre
            Button(self, text="OK", bg="#A65400", fg="white", activebackground="#BF7830",
                   activeforeground="black", font=("Comic sans Ms", 10, 'bold'),
                   command=self.destroy).grid(row=i+2, column=1, pady=5)

        else:
            Label(self, text="Afin de comptabiliser vos scores, \n n'oubliez pas de vous inscrire !",
                  font=("Comic sans Ms", 14), justify=CENTER, bg='#2C2F10',
                  fg='#F0E39E').grid(row=1, column=1, columnspan=2, padx=5)
            Button(self, text="Login", bg="#A65400", fg='white', activebackground="#BF7830",
                   activeforeground='black', font=("Comic sans Ms", 10, 'bold'),
                   command=boss.login).grid(row=2, column=1, pady=5)
            Button(self, text="Quitter", bg="#A65400", fg='white', activebackground="#BF7830",
                   activeforeground='black', font=("Comic sans Ms", 10, 'bold'),
                   command=self.destroy).grid(row=2, column=2, pady=5)

    def trierScores(self, pseudo):
        """
        trierScores(str pseudo) --> list
        Récupère tous les scores situés dans chacune des lignes du fichier associé à
        <pseudo> et retourne la liste de ces scores sous forme de tuples (points, mot, date)
        triée en fonction du nombre de points
        """
        
        nomFichier = "annexes/joueurs/"+pseudo+'.txt' 
        fichier = open(nomFichier, 'r', encoding='utf8')

        resultats = []

        fichier.readline()                                      # la première ligne contient le titre

        score = fichier.readline()

        while score != "":
            date = score.split(":")[0]                          # récupère la date
            mot = (score.split(":")[1]).split("@")[0]           # récupère le mot
            points = (score.split(":")[1]).split("@")[1][:-1]   # récupère les points associés
            
            resultats.append((int(points), mot, date))               # ajoute le tuple (points, mot, date) à la liste
            
            score = fichier.readline()

        fichier.close()

        resultats.sort()
        resultats.reverse()                                     # trie la liste de manière décroissante

        return resultats
        
            
#### Partie principale du programme ###
#-------------------------------------#
monApp = Application()          # instanciation du jeu
monApp.mainloop()               # lancement du réceptionnaire d'événements
monApp.destroy()                # destruction du jeu lors de la fermeture finale des fenetres   
