"""
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()