import tkinter as tk
from tkinter import messagebox
import random

# --- Fonctions utilitaires ---

def get_random_move():
    """Renvoie un coup aléatoire parmi 'pierre', 'feuille' ou 'ciseau'."""
    return random.choice(["pierre", "feuille", "ciseau"])

def move_that_beats(move):
    """Renvoie le coup qui bat le coup donné."""
    mapping = {"pierre": "feuille", "feuille": "ciseau", "ciseau": "pierre"}
    return mapping.get(move, get_random_move())

def get_probability_distribution(context, ngram_counts, n):
    """
    Pour un contexte donné (un tuple de coups) dans le n‑gram de taille n,
    retourne la distribution (dictionnaire coup -> fréquence relative)
    des coups suivants, ou None si aucune donnée.
    """
    if context in ngram_counts[n]:
        counts = ngram_counts[n][context]
        total = sum(counts.values())
        if total > 0:
            return {move: counts[move] / total for move in counts}
    return None

def predict_next_move(history, ngram_counts, max_n=10):
    """
    Parcourt, du plus long au plus court, le contexte constitué des derniers n coups
    (n allant de max_n à 1) de l'historique (c.-à-d. la séquence précédant le coup à venir)
    et retourne une prédiction pour le prochain coup humain, la distribution,
    le niveau utilisé et la confiance.
    S'il n'y a pas de donnée, renvoie (None, None, None, None).
    """
    for n in range(max_n, 0, -1):
        if len(history) >= n:
            context = tuple(history[-n:])
            dist = get_probability_distribution(context, ngram_counts, n)
            if dist:
                best_prob = max(dist.values())
                best_moves = [m for m, p in dist.items() if p == best_prob]
                return random.choice(best_moves), dist, n, best_prob
    return None, None, None, None

def predict_table(history, ngram_counts, max_n=10):
    """
    Construit un tableau (liste) contenant, pour chaque niveau (de 1 à max_n) dont
    le contexte est disponible, un tuple :
      (niveau, contexte, distribution, coup_prédit, confiance)
    """
    table = []
    for n in range(1, max_n+1):
        if len(history) >= n:
            context = tuple(history[-n:])
            dist = get_probability_distribution(context, ngram_counts, n)
            if dist:
                best_prob = max(dist.values())
                best_moves = [m for m, p in dist.items() if p == best_prob]
                table.append((n, context, dist, random.choice(best_moves), best_prob))
    return table

def update_ngram_counts(history, ngram_counts, max_n=10):
    """
    Dès qu'un nouveau coup est joué, enregistre dans la table de n‑grams
    la transition depuis le contexte (les n coups précédant le coup courant)
    vers le coup courant.
    Pour chaque n de 1 à max_n, si history contient au moins n+1 coups,
    le contexte est history[-(n+1):-1].
    """
    for n in range(1, min(max_n, len(history)) + 1):
        if len(history) > n:
            context = tuple(history[-(n+1):-1])
            next_move = history[-1]
            if context not in ngram_counts[n]:
                ngram_counts[n][context] = {"pierre": 0, "feuille": 0, "ciseau": 0}
            ngram_counts[n][context][next_move] += 1

def determine_winner(comp_move, human_move):
    """
    Compare les coups et renvoie :
      1  si l'ordinateur gagne,
     -1  si le joueur gagne,
      0  en cas d'égalité.
    """
    if comp_move == human_move:
        return 0
    if (comp_move == "pierre" and human_move == "ciseau") or \
       (comp_move == "feuille" and human_move == "pierre") or \
       (comp_move == "ciseau" and human_move == "feuille"):
        return 1
    return -1

# --- Interface graphique et logique du jeu ---

class RPSGameGUIParallel:
    def __init__(self, master):
        self.master = master
        master.title("Pierre-Feuille-Ciseau – Comparaison d'Algorithmes")

        # Paramètres de la partie (nombre de rounds choisissable)
        self.rounds = 100
        self.current_round = 0
        self.score_random = 0
        self.score_predictive = 0

        # Historique des coups du joueur et table de n‑grams (pour l'algorithme prédictif)
        self.history = []
        self.max_n = 10
        self.ngram_counts = { n: {} for n in range(1, self.max_n+1) }

        # Pour le round courant, on stocke les coups calculés par chaque algorithme
        self.next_random_move = None
        self.next_predictive_move = None  # calculé via la prédiction : move_that_beats(predicted)
        # Pour l'algorithme prédictif, on garde aussi la prédiction (le coup humain prédit),
        # la distribution, le niveau utilisé et la confiance
        self.next_predicted_human = None
        self.next_distribution = None
        self.next_level = None
        self.next_confidence = None

        self.create_widgets()

    def create_widgets(self):
        top_frame = tk.Frame(self.master)
        top_frame.pack(pady=10)

        # Choix du nombre de rounds
        rounds_frame = tk.LabelFrame(top_frame, text="Nombre de rounds")
        rounds_frame.grid(row=0, column=0, padx=10)
        rounds_options = ["10", "20", "50", "100"]
        self.rounds_choice = tk.StringVar(value="100")
        tk.OptionMenu(rounds_frame, self.rounds_choice, *rounds_options).pack()

        # Bouton pour démarrer
        self.start_button = tk.Button(top_frame, text="Commencer le jeu", command=self.start_game)
        self.start_button.grid(row=0, column=1, padx=10)

        self.game_frame = tk.Frame(self.master)
        self.game_frame.pack(pady=10)

        self.info_label = tk.Label(self.master, text="", justify="left", font=("Consolas", 10))
        self.info_label.pack(pady=5)

    def start_game(self):
        self.start_button.config(state="disabled")
        self.rounds = int(self.rounds_choice.get())
        # Initialisation du premier round :
        # Pour le 1er round, il n'y a pas d'historique → on choisit aléatoirement pour les deux algorithmes.
        self.next_random_move = get_random_move()
        self.next_predictive_move = get_random_move()
        self.next_predicted_human = "N/A"
        self.next_distribution = {}
        self.next_level = "N/A"
        self.next_confidence = "N/A"
        self.update_info("Début de partie")

        # Boutons pour jouer
        self.btn_pierre = tk.Button(self.game_frame, text="pierre", width=10, command=lambda: self.play_round("pierre"))
        self.btn_feuille = tk.Button(self.game_frame, text="feuille", width=10, command=lambda: self.play_round("feuille"))
        self.btn_ciseau = tk.Button(self.game_frame, text="ciseau", width=10, command=lambda: self.play_round("ciseau"))
        self.btn_pierre.grid(row=0, column=0, padx=5, pady=5)
        self.btn_feuille.grid(row=0, column=1, padx=5, pady=5)
        self.btn_ciseau.grid(row=0, column=2, padx=5, pady=5)

    def play_round(self, human_move):
        # Pour le round courant, on utilise les coups déjà calculés :
        random_move = self.next_random_move
        predictive_move = self.next_predictive_move

        # Détermination des résultats pour chaque algorithme
        res_random = determine_winner(random_move, human_move)
        res_predictive = determine_winner(predictive_move, human_move)
        if res_random == 1:
            round_result_random = "Gagné"
            self.score_random += 1
        elif res_random == -1:
            round_result_random = "Perdu"
        else:
            round_result_random = "Égalité"

        if res_predictive == 1:
            round_result_predictive = "Gagné"
            self.score_predictive += 1
        elif res_predictive == -1:
            round_result_predictive = "Perdu"
        else:
            round_result_predictive = "Égalité"

        # Mise à jour de l'historique et de la table des n‑grams (pour l'algorithme prédictif)
        self.history.append(human_move)
        update_ngram_counts(self.history, self.ngram_counts, self.max_n)

        self.current_round += 1

        # Calcul pour le prochain round :
        self.next_random_move = get_random_move()
        # Pour la prédiction, on se base sur l'historique (avant le prochain coup)
        pred, dist, level, conf = predict_next_move(self.history, self.ngram_counts, self.max_n)
        if pred is None:
            self.next_predicted_human = "NA"
            self.next_distribution = {}
            self.next_level = "NA"
            self.next_confidence = "NA"
            self.next_predictive_move = get_random_move()
        else:
            self.next_predicted_human = pred
            self.next_distribution = dist
            self.next_level = level
            self.next_confidence = conf
            self.next_predictive_move = move_that_beats(pred)

        # Construction du tableau de prédictions (détails par niveau)
        table = predict_table(self.history, self.ngram_counts, self.max_n)
        if table:
            table_lines = []
            for n, context, distribution, pred_val, conf_val in table:
                line = f"Niv {n}: contexte {context} -> {pred_val} ({conf_val:.2f}) | " + \
                       ", ".join([f"{m}:{distribution[m]:.2f}" for m in distribution])
                table_lines.append(line)
            table_str = "\n".join(table_lines)
        else:
            table_str = "Aucune donnée"

        message = (
            f"Round {self.current_round}/{self.rounds}\n"
            f"Votre coup : {human_move}\n\n"
            f"[Algorithme Aléatoire]\n"
            f"  Coup : {random_move}  →  Résultat : {round_result_random}\n\n"
            f"[Algorithme Prédictif]\n"
            f"  Prédiction du coup humain (basée sur l'historique) : {self.next_predicted_human}\n"
            f"  Coup joué : {predictive_move}  →  Résultat : {round_result_predictive}\n\n"
            f"Scores : Aléatoire = {self.score_random} | Prédictif = {self.score_predictive}\n\n"
            f"Tableau des prédictions par niveau :\n{table_str}"
        )
        self.info_label.config(text=message)

        if self.current_round >= self.rounds:
            self.btn_pierre.config(state="disabled")
            self.btn_feuille.config(state="disabled")
            self.btn_ciseau.config(state="disabled")
            final_msg = (
                f"Fin des parties !\n"
                f"Score final :\n"
                f"Aléatoire = {self.score_random}\n"
                f"Prédictif = {self.score_predictive}"
            )
            messagebox.showinfo("Fin du jeu", final_msg)

    def update_info(self, text=""):
        self.info_label.config(text=text)

if __name__ == "__main__":
    root = tk.Tk()
    game = RPSGameGUIParallel(root)
    root.mainloop()
