Newer
Older
python_M_mind / src / gui.py
"""
Interface graphique Tkinter avancée pour MasterMind
"""

import tkinter as tk
from tkinter import ttk
from typing import List, Tuple
from game_logic import MasterMindGame, guess_to_colors, feedback_to_string
from config import (
    COLORS,
    CIRCLE_DIAMETER,
    CIRCLE_SPACING,
    FEEDBACK_DIAMETER,
    FEEDBACK_SPACING,
    GRID_PADDING,
    ROW_HEIGHT,
    BG_COLOR,
    BUTTON_COLOR,
    SELECTED_BORDER,
    DEFAULT_BORDER,
    WINDOW_WIDTH,
    WINDOW_HEIGHT,
    WINDOW_TITLE,
)


def create_octagon(canvas: tk.Canvas, x1: int, y1: int, x2: int, y2: int, **kwargs):
    """Dessine un octogone (polygone à 8 côtés)"""
    # Coins d'un rectangle
    left, top, right, bottom = x1, y1, x2, y2
    width = right - left
    height = bottom - top
    cut = min(width, height) // 4  # Taille des coins coupés

    # Points de l'octogone (sens horaire)
    points = [
        left + cut,
        top,  # haut-gauche (début coupe)
        right - cut,
        top,  # haut-droit (début coupe)
        right,
        top + cut,  # droite-haut (début coupe)
        right,
        bottom - cut,  # droite-bas (début coupe)
        right - cut,
        bottom,  # bas-droit (début coupe)
        left + cut,
        bottom,  # bas-gauche (début coupe)
        left,
        bottom - cut,  # gauche-bas (début coupe)
        left,
        top + cut,  # gauche-haut (début coupe)
    ]
    return canvas.create_polygon(points, **kwargs)


class MasterMindGUI:
    """Interface Tkinter pour MasterMind"""

    def __init__(self):
        self.root = tk.Tk()
        self.root.title(WINDOW_TITLE)
        self.root.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
        self.root.configure(bg=BG_COLOR)

        # Jeu
        self.game = MasterMindGame()

        # Variables pour la sélection de couleurs
        self.selected_colors = [0] * 4  # Indices des couleurs choisies
        self.guess_buttons = []  # Canvas des 4 positions à deviner

        # Style ttk
        self.style = ttk.Style()
        self.style.theme_use("clam")
        # ICI: configuration de la taille de police des boutons
        # Modifie 'font' pour changer la taille (9 par défaut, 8 pour plus petit)
        self.style.configure(
            "TButton", padding=3, background=BUTTON_COLOR, font=("Helvetica", 8)
        )
        self.style.configure("Title.TLabel", font=("Helvetica", 16, "bold"))

        # Layout principal en grille
        self.root.columnconfigure(1, weight=1)  # Colonne principale extensible
        self.root.rowconfigure(2, weight=1)  # Ligne historique extensible

        # Construction de l'interface
        self._build_palette()  # Colonne 0 (avec boutons de contrôle)
        self._build_main_area()  # Colonne 1

        # Initialiser l'affichage
        self._update_guess_display()

    def _build_palette(self):
        """Palette verticale de boutons octogonaux (côté gauche) + boutons de contrôle"""
        palette_frame = ttk.Frame(self.root, padding=10)
        palette_frame.grid(row=0, column=0, rowspan=4, sticky="ns", padx=(10, 5))

        ttk.Label(palette_frame, text="Couleurs", font=("Helvetica", 12, "bold")).pack(
            pady=(0, 10)
        )

        self.palette_canvas = tk.Canvas(
            palette_frame, width=70, height=350, bg=BG_COLOR, highlightthickness=0
        )
        self.palette_canvas.pack()

        # Créer les octogones pour chaque couleur
        self.color_buttons = []
        button_size = 45
        spacing = 55
        start_y = 15

        for idx, (name, color) in enumerate(COLORS):
            x1 = 15
            y1 = start_y + idx * spacing
            x2 = x1 + button_size
            y2 = y1 + button_size

            # Octogone
            oct_id = create_octagon(
                self.palette_canvas,
                x1,
                y1,
                x2,
                y2,
                fill=color,
                outline="black",
                width=2,
                tags=f"color_{idx}",
            )
            self.palette_canvas.tag_bind(
                oct_id, "<Button-1>", lambda e, c=idx: self._select_color(c)
            )

            # Texte (nom de la couleur)
            text_id = self.palette_canvas.create_text(
                (x1 + x2) / 2,
                (y1 + y2) / 2,
                text=name[:3].upper(),
                font=("Helvetica", 8, "bold"),
                fill="white" if self._is_dark(color) else "black",
                tags=f"text_{idx}",
            )
            self.palette_canvas.tag_bind(
                text_id, "<Button-1>", lambda e, c=idx: self._select_color(c)
            )

            self.color_buttons.append(oct_id)

        # Boutons de contrôle sous la palette
        control_frame = ttk.Frame(palette_frame)
        control_frame.pack(pady=(20, 0))

        self.validate_btn = ttk.Button(
            control_frame, text="Valider", command=self._on_validate, width=8
        )
        self.validate_btn.pack(pady=3)

        self.new_game_btn = ttk.Button(
            control_frame, text="Rejouer", command=self._on_new_game, width=8
        )
        self.new_game_btn.pack(pady=3)

        self.reveal_btn = ttk.Button(
            control_frame, text="Solution", command=self._on_reveal, width=8
        )
        self.reveal_btn.pack(pady=3)

    def _build_main_area(self):
        """Zone principale (droite) avec titre, guess, historique"""
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.grid(row=0, column=1, rowspan=4, sticky="nsew", padx=(5, 10))

        # Configuration de la grille interne
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(2, weight=1)  # Historique extensible

        # Titre
        title = ttk.Label(main_frame, text="MASTERMIND", style="Title.TLabel")
        title.grid(row=0, column=0, pady=(0, 10))

        # Zone de sélection du guess (votre combinaison)
        guess_frame = ttk.LabelFrame(main_frame, text="Votre combinaison", padding=10)
        guess_frame.grid(row=1, column=0, pady=(0, 10), sticky="ew")
        guess_frame.columnconfigure(tuple(range(4)), weight=1)

        # Cercles cliquables pour le guess
        self.guess_buttons = []
        for i in range(4):
            canvas = tk.Canvas(
                guess_frame,
                width=CIRCLE_DIAMETER + 4,
                height=CIRCLE_DIAMETER + 4,
                bg=BG_COLOR,
                highlightthickness=0,
            )
            canvas.grid(row=0, column=i, padx=5, pady=5)
            canvas.create_oval(
                2,
                2,
                CIRCLE_DIAMETER + 2,
                CIRCLE_DIAMETER + 2,
                fill=COLORS[0][1],
                outline=SELECTED_BORDER,
                width=2,
                tags=f"circle_{i}",
            )
            canvas.bind("<Button-1>", lambda e, idx=i: self._on_guess_circle_click(idx))
            self.guess_buttons.append(canvas)

        # Historique
        history_frame = ttk.LabelFrame(main_frame, text="Historique", padding=5)
        history_frame.grid(row=2, column=0, sticky="nsew", pady=(10, 0))
        history_frame.columnconfigure(0, weight=1)
        history_frame.rowconfigure(0, weight=1)

        # Canvas scrollable pour l'historique
        self.history_canvas = tk.Canvas(history_frame, bg=BG_COLOR, height=250)
        self.history_canvas.grid(row=0, column=0, sticky="nsew")

        scrollbar = ttk.Scrollbar(
            history_frame, orient=tk.VERTICAL, command=self.history_canvas.yview
        )
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.history_canvas.configure(yscrollcommand=scrollbar.set)

        # Frame conteneur
        self.history_frame = ttk.Frame(self.history_canvas)
        self.history_canvas.create_window(
            (0, 0), window=self.history_frame, anchor="nw"
        )
        self.history_frame.bind(
            "<Configure>",
            lambda e: self.history_canvas.configure(
                scrollregion=self.history_canvas.bbox("all")
            ),
        )

        # Barre de statut (en bas de la fenêtre principale)
        self.status_label = ttk.Label(
            self.root, text="Prêt à jouer!", relief=tk.SUNKEN, anchor=tk.W
        )
        self.status_label.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(5, 0))

    def _is_dark(self, hex_color: str) -> bool:
        """Détermine si une couleur hexadécimale est sombre (pour texte blanc/noir)"""
        hex_color = hex_color.lstrip("#")
        r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
        luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
        return luminance < 0.5

    def _on_guess_circle_click(self, position: int):
        """Clic sur un cercle de guess -> change sa couleur"""
        self.selected_colors[position] = (self.selected_colors[position] + 1) % len(
            COLORS
        )
        self._update_guess_display()

    def _select_color(self, color_idx: int):
        """Sélection d'une couleur dans la palette -> affecte toutes les positions"""
        for i in range(4):
            self.selected_colors[i] = color_idx
        self._update_guess_display()

    def _update_guess_display(self):
        """Met à jour l'affichage des cercles de guess"""
        for i, color_idx in enumerate(self.selected_colors):
            color_hex = COLORS[color_idx][1]
            canvas = self.guess_buttons[i]
            canvas.itemconfig(f"circle_{i}", fill=color_hex)

    def _on_validate(self):
        """Valide le guess actuel"""
        try:
            won, feedback = self.game.make_guess(self.selected_colors.copy())
            self._add_history_row(self.selected_colors.copy(), feedback, won)
            self._update_status_after_guess(won, feedback)

            if self.game.game_over:
                if won:
                    self.status_label.config(text="🎉 Félicitations! Vous avez gagné!")
                else:
                    self.status_label.config(
                        text=f"💀 Perdu! Le code était: {guess_to_colors(self.game.reveal_secret())}"
                    )
                self.validate_btn.config(state="disabled")
            else:
                # Reset selection
                self.selected_colors = [0] * 4
                self._update_guess_display()

        except ValueError as e:
            self.status_label.config(text=str(e))

    def _on_new_game(self):
        """Nouvelle partie"""
        self.game.reset()
        self._clear_history()
        self.selected_colors = [0] * 4
        self._update_guess_display()
        self.validate_btn.config(state="normal")
        self.status_label.config(text="Nouvelle partie!")

    def _on_reveal(self):
        """Révèle le code secret (triche)"""
        secret = self.game.reveal_secret()
        colors = guess_to_colors(secret)
        self.status_label.config(text=f"Code secret: {colors}")

    def _update_status_after_guess(self, won: bool, feedback: Tuple[int, int]):
        """Met à jour le message de statut après un guess"""
        black, white = feedback
        remaining = self.game.get_remaining_attempts()
        if won:
            msg = f"Bravo! Trouvé en {len(self.game.attempts)} tentatives!"
        else:
            msg = f"Feedback: {black} noir(s), {white} blanc(s) - {remaining} essais restants"
        self.status_label.config(text=msg)

    def _clear_history(self):
        """Efface l'historique visuel"""
        for widget in self.history_frame.winfo_children():
            widget.destroy()

    def _add_history_row(self, guess: List[int], feedback: Tuple[int, int], won: bool):
        """Ajoute une ligne dans l'historique"""
        row = len(self.game.attempts) - 1
        frame = ttk.Frame(self.history_frame)
        frame.grid(row=row, column=0, pady=5, sticky="ew")

        # Numéro de tentative
        ttk.Label(frame, text=f"#{row+1}", width=5).grid(row=0, column=0, padx=5)

        # Cercles de couleurs du guess
        guess_frame = ttk.Frame(frame)
        guess_frame.grid(row=0, column=1, padx=10)

        for i, color_idx in enumerate(guess):
            canvas = tk.Canvas(
                guess_frame,
                width=CIRCLE_DIAMETER,
                height=CIRCLE_DIAMETER,
                bg=BG_COLOR,
                highlightthickness=0,
            )
            canvas.grid(row=0, column=i, padx=2)
            canvas.create_oval(
                2,
                2,
                CIRCLE_DIAMETER - 2,
                CIRCLE_DIAMETER - 2,
                fill=COLORS[color_idx][1],
                outline="black",
                width=1,
            )

        # Feedback (petits cercles)
        feedback_frame = ttk.Frame(frame)
        feedback_frame.grid(row=0, column=2, padx=10)

        black, white = feedback
        # D'abord les noirs (ordre important)
        for i in range(black):
            self._draw_feedback_circle(feedback_frame, i, "black")

        # Puis les blancs
        for i in range(white):
            self._draw_feedback_circle(feedback_frame, black + i, "white")

        # Indicateur gagné
        if won:
            ttk.Label(
                frame, text="✓", foreground="green", font=("Helvetica", 16, "bold")
            ).grid(row=0, column=3, padx=10)

    def _draw_feedback_circle(self, parent: ttk.Frame, index: int, color: str):
        """Dessine un petit cercle de feedback (noir ou blanc)"""
        canvas = tk.Canvas(
            parent,
            width=FEEDBACK_DIAMETER,
            height=FEEDBACK_DIAMETER,
            bg=BG_COLOR,
            highlightthickness=0,
        )
        col = index % 3
        row = index // 3
        canvas.grid(row=row, column=col, padx=1, pady=1)
        canvas.create_oval(
            1,
            1,
            FEEDBACK_DIAMETER - 1,
            FEEDBACK_DIAMETER - 1,
            fill=color,
            outline="gray",
            width=1,
        )

    def run(self):
        """Lance la boucle principale de l'application"""
        self.root.mainloop()