From 71ff931b5ccf7244d8feeca785c31c28e2c5b939 Mon Sep 17 00:00:00 2001 From: BYolivia Date: Sat, 6 Dec 2025 13:37:51 +0100 Subject: [PATCH] feat(vista): Refactoriza ventana principal (main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactoriza la ventana principal para modularizar y mejorar la estructura. * Reestructura ventana principal con módulos. * Integra clases modulares para cada pestaña. * Corrige errores de inicialización y dependencias. * Agrega view_scrapping.py con NavegadorPanel. * Refactoriza musicReproductor.py. * Modifica trafficMeter.py. * Modifica getWeather.py. * Elimina __main__.py. * Agrega logica/T2/buscaMinas.py con la lógica del juego. * Agrega vista/ventana_buscaMinas.py para minijuego. * Corrige menores en Readme.md. --- Readme.md | 5 +- logica/T2/buscaMinas.py | 204 +++++++++++++++++++++++++++++++++ logica/auxiliar/__main__.py | 106 ------------------ logica/auxiliar/add_audio.py | 80 +++++++++++++ vista/panel_lateral.py | 46 ++++++-- vista/ventana_buscaMinas.py | 212 +++++++++++++++++++++++++++++++++++ 6 files changed, 536 insertions(+), 117 deletions(-) create mode 100644 logica/T2/buscaMinas.py delete mode 100644 logica/auxiliar/__main__.py create mode 100644 logica/auxiliar/add_audio.py create mode 100644 vista/ventana_buscaMinas.py diff --git a/Readme.md b/Readme.md index d79b9a8..08bf68a 100644 --- a/Readme.md +++ b/Readme.md @@ -9,10 +9,9 @@ python-vlc => pip install python-vlc bs4 => pip install bs4 - - requests => pip install requests + ## Como Ejecutar ## > [!NOTE] > Desde la carpeta anterior @@ -46,6 +45,6 @@ python -m ProyectoGlobal 4. ~~Scraping~~ -5. ~~Juego de los camellos~~ / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos) +5. ~~Juego de los camellos / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos)~~ 6. ~~Música de fondo (reproducción de mp3 o midi)~~ \ No newline at end of file diff --git a/logica/T2/buscaMinas.py b/logica/T2/buscaMinas.py new file mode 100644 index 0000000..9be69f3 --- /dev/null +++ b/logica/T2/buscaMinas.py @@ -0,0 +1,204 @@ +# Módulo: logica/T2/buscaMinas.py + +import random + + +class BuscaMinas: + """ + Lógica de un Buscaminas con regla de 'primer clic seguro' y banderas. + """ + + def __init__(self, rows=16, cols=16, mines=40): + self.rows = rows + self.cols = cols + self.mines = mines + self.board = [] # El tablero real (minas: 'M', números: 0-8) + self.revealed = [] # El estado visible (True/False) + self.flagged = [] # Estado de la bandera (True/False) + self.first_click = True # Bandera para detectar el primer clic + self.game_over = False + self.won = False + self.setup_board() + + def setup_board(self): + """Inicializa un tablero vacío y el estado de juego, esperando el primer clic.""" + + self.board = [[0 for _ in range(self.cols)] for _ in range(self.rows)] + self.revealed = [[False for _ in range(self.cols)] for _ in range(self.rows)] + self.flagged = [[False for _ in range(self.cols)] for _ in range(self.rows)] + self.game_over = False + self.won = False + self.first_click = True + + print(f"[Buscaminas] Tablero vacío {self.rows}x{self.cols} listo. Esperando primer clic seguro.") + + def _place_mines_safely(self, start_r, start_c): + """ + Coloca las minas *después* del primer clic, + garantizando que la zona 3x3 alrededor de (start_r, start_c) esté libre. + """ + + # 1. Definir posiciones prohibidas (3x3 alrededor del clic inicial) + safe_positions = set() + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + nr, nc = start_r + dr, start_c + dc + if 0 <= nr < self.rows and 0 <= nc < self.cols: + safe_positions.add((nr, nc)) + + available_positions = [] + for r in range(self.rows): + for c in range(self.cols): + if (r, c) not in safe_positions: + available_positions.append((r, c)) + + # 2. Elegir la posición de las minas + mine_positions = random.sample(available_positions, self.mines) + + # 3. Colocar Minas + for r, c in mine_positions: + self.board[r][c] = 'M' + + # 4. Calcular números adyacentes + for r in range(self.rows): + for c in range(self.cols): + if self.board[r][c] != 'M': + self.board[r][c] = self._count_adjacent_mines(r, c) + + print(f"[Buscaminas] Minas y números colocados. Zona de clic ({start_r},{start_c}) segura.") + + def _count_adjacent_mines(self, r, c): + """Cuenta el número de minas en las 8 casillas adyacentes.""" + count = 0 + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + if dr == 0 and dc == 0: + continue + nr, nc = r + dr, c + dc + + if 0 <= nr < self.rows and 0 <= nc < self.cols: + if self.board[nr][nc] == 'M': + count += 1 + return count + + def toggle_flag(self, r, c): + """ + Alterna el estado de la bandera en una celda. + Solo se permite si la celda no ha sido revelada y el juego no ha terminado. + Retorna True si el estado del tablero se modificó, False en caso contrario. + """ + if self.game_over or self.revealed[r][c]: + return False + + # Alternar la bandera + self.flagged[r][c] = not self.flagged[r][c] + return True + + def reveal_cell(self, r, c): + """ + Revela una celda al ser clickeada (clic izquierdo). + Retorna: (str, bool) -> (Mensaje, ¿Juego terminado?) + """ + if self.game_over or self.revealed[r][c]: + return "", False + + # IMPEDIR REVELAR SI HAY BANDERA COLOCADA + if self.flagged[r][c]: + return "❌ Debes quitar la bandera para revelar la celda.", False + + # Si es el primer clic, colocamos las minas de forma segura + if self.first_click: + self._place_mines_safely(r, c) + self.first_click = False + + self.revealed[r][c] = True + + # Caso 1: Mina + if self.board[r][c] == 'M': + self.game_over = True + return "💥 ¡BOOM! Juego terminado.", True + + # Caso 2: Cero (Expansión) + elif self.board[r][c] == 0: + self._expand_zeros(r, c) + + # Caso 3: Número (Solo revelar) + + # Verificar victoria después de la jugada + if self._check_win(): + self.game_over = True + self.won = True + return "🏆 ¡Ganaste! Has revelado todas las casillas seguras.", True + + return "", False + + def _expand_zeros(self, r, c): + """Algoritmo de inundación para revelar áreas vacías (BFS).""" + + queue = [(r, c)] + + while queue: + curr_r, curr_c = queue.pop(0) + + for dr in [-1, 0, 1]: + for dc in [-1, 0, 1]: + nr, nc = curr_r + dr, curr_c + dc + + if 0 <= nr < self.rows and 0 <= nc < self.cols: + if not self.revealed[nr][nc]: + # Si hay una bandera, no la quitamos, pero no la propagamos + if self.flagged[nr][nc]: + continue + + self.revealed[nr][nc] = True + + if self.board[nr][nc] == 0: + queue.append((nr, nc)) + + def _check_win(self): + """Verifica si todas las casillas no-mina han sido reveladas.""" + safe_cells_revealed = 0 + total_safe_cells = self.rows * self.cols - self.mines + + for r in range(self.rows): + for c in range(self.cols): + if self.revealed[r][c] and self.board[r][c] != 'M': + safe_cells_revealed += 1 + + return safe_cells_revealed == total_safe_cells + + def get_board_state(self): + """Retorna una matriz con el estado visible del juego (para la UI), incluyendo banderas y el resultado final.""" + state = [] + + for r in range(self.rows): + row = [] + for c in range(self.cols): + + if self.revealed[r][c]: + # 1. Revelada: muestra el contenido real + row.append(str(self.board[r][c])) + elif self.flagged[r][c] and not self.game_over: + # 2. Bandera (juego en curso) + row.append('F') + elif self.game_over: + # 3. Analizar estado final (solo si el juego terminó) + if self.flagged[r][c]: + if self.board[r][c] == 'M': + # 3a. Bandera CORRECTA + row.append('F+') + else: + # 3b. Bandera INCORRECTA (marcada en celda segura) + row.append('F-') + elif self.board[r][c] == 'M': + # 3c. Mina sin marcar + row.append('M') + else: + # 3d. Celda segura, sin revelar y sin bandera + row.append(' ') + else: + # 4. Celda oculta normal (juego en curso) + row.append(' ') + + state.append(row) + return state \ No newline at end of file diff --git a/logica/auxiliar/__main__.py b/logica/auxiliar/__main__.py deleted file mode 100644 index 9dad83f..0000000 --- a/logica/auxiliar/__main__.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import os - -# --- NOMBRE DEL ARCHIVO --- -NOMBRE_FICHERO = "radios.json" - - -def cargar_emisoras(): - """Carga las emisoras existentes desde el archivo, o retorna una lista vacía si no existe.""" - if os.path.exists(NOMBRE_FICHERO): - try: - with open(NOMBRE_FICHERO, 'r', encoding='utf-8') as f: - return json.load(f) - except json.JSONDecodeError: - print(f"⚠️ Error al leer el archivo {NOMBRE_FICHERO}. Creando una lista nueva.") - return [] - return [] - - -def guardar_emisoras(emisoras): - """Guarda la lista de emisoras en formato JSON en el archivo.""" - try: - with open(NOMBRE_FICHERO, 'w', encoding='utf-8') as f: - # Usamos indent=4 para que el JSON sea legible - json.dump(emisoras, f, indent=4, ensure_ascii=False) - print(f"\n✅ ¡Éxito! El archivo '{NOMBRE_FICHERO}' ha sido guardado.") - except Exception as e: - print(f"\n❌ Error al intentar guardar el archivo: {e}") - - -def crear_nueva_emisora(emisoras): - """Pide al usuario los datos de una nueva emisora y la añade a la lista.""" - print("\n--- Crear Nueva Emisora ---") - - # --- CAMPOS OBLIGATORIOS --- - nombre = input("▶️ Nombre de la radio (ej: Jazz Clásico): ").strip() - url = input("▶️ URL del Stream (ej: http://stream.com/live.mp3): ").strip() - - if not nombre or not url: - print("❌ El nombre y la URL son obligatorios. Inténtalo de nuevo.") - return - - # --- CAMPOS OPCIONALES --- - pais = input("▶️ País (ej: ES, DE) [Opcional]: ").strip() - genero = input("▶️ Género (ej: Pop, Noticias) [Opcional]: ").strip() - - nueva_emisora = { - "nombre": nombre, - "url_stream": url, - "pais": pais if pais else None, - "genero": genero if genero else None - } - - emisoras.append(nueva_emisora) - print(f"\n✅ Emisora '{nombre}' añadida a la lista en memoria.") - - -def mostrar_emisoras(emisoras): - """Muestra la lista de emisoras actuales en memoria.""" - if not emisoras: - print("\nLista de emisoras vacía. Utiliza la opción 1 para añadir una.") - return - - print(f"\n--- Emisoras Actuales en Memoria ({len(emisoras)} en total) ---") - for i, e in enumerate(emisoras): - print(f"\nID: {i + 1}") - print(f" Nombre: {e.get('nombre', 'N/D')}") - print(f" URL: {e.get('url_stream', 'N/D')}") - print(f" País: {e.get('pais', 'N/D')}") - print(f" Género: {e.get('genero', 'N/D')}") - print("--------------------------------------------------") - - -def menu_principal(): - """Función principal que ejecuta el menú interactivo.""" - - # Cargar las emisoras al iniciar (si el archivo existe) - emisoras_en_memoria = cargar_emisoras() - - while True: - print("\n" + "=" * 30) - print("📻 EDITOR DE CONFIGURACIÓN DE RADIOS 📻") - print("=" * 30) - print("1. Crear y Añadir Nueva Emisora") - print("2. Mostrar Emisoras en Memoria") - print(f"3. Guardar Lista en '{NOMBRE_FICHERO}'") - print("4. Salir (Sin guardar los cambios en memoria)") - print("-" * 30) - - opcion = input("Elige una opción (1-4): ").strip() - - if opcion == '1': - crear_nueva_emisora(emisoras_en_memoria) - elif opcion == '2': - mostrar_emisoras(emisoras_en_memoria) - elif opcion == '3': - guardar_emisoras(emisoras_en_memoria) - elif opcion == '4': - print("\nSaliendo del editor. ¡Hasta pronto!") - break - else: - print("Opción no válida. Por favor, selecciona un número del 1 al 4.") - - -if __name__ == "__main__": - menu_principal() \ No newline at end of file diff --git a/logica/auxiliar/add_audio.py b/logica/auxiliar/add_audio.py new file mode 100644 index 0000000..c0c3631 --- /dev/null +++ b/logica/auxiliar/add_audio.py @@ -0,0 +1,80 @@ +# Módulo: logica/auxiliar/add_audio.py + +import json +import os +import threading + +# 🔑 RUTA CONFIRMADA: Desde la raíz del proyecto +NOMBRE_FICHERO = os.path.join("res", "radios.json") + + +class RadioManager: + """ + Gestiona la lectura, escritura y adición de emisoras en el archivo radios.json. + Utiliza un Lock para asegurar la seguridad al escribir el archivo. + """ + + def __init__(self, filename=NOMBRE_FICHERO): + self.filename = filename + # Bloqueo para operaciones de archivo + self._lock = threading.Lock() + self.emisoras_cargadas = self.cargar_emisoras() + + # ------------------------------------------------------------- + # GESTIÓN DE ARCHIVO + # ------------------------------------------------------------- + + def cargar_emisoras(self): + """Carga las emisoras existentes desde el archivo.""" + with self._lock: + if os.path.exists(self.filename): + try: + with open(self.filename, 'r', encoding='utf-8') as f: + return json.load(f) + except json.JSONDecodeError: + print(f"⚠️ Error al leer el archivo {self.filename}. Retornando lista vacía.") + return [] + return [] + + def guardar_emisoras(self): + """Guarda la lista de emisoras actual en memoria en formato JSON.""" + with self._lock: + try: + with open(self.filename, 'w', encoding='utf-8') as f: + json.dump(self.emisoras_cargadas, f, indent=4, ensure_ascii=False) + print(f"\n✅ [RadioManager] Archivo '{self.filename}' guardado.") + return True + except Exception as e: + print(f"\n❌ [RadioManager] Error al intentar guardar el archivo: {e}") + return False + + # ------------------------------------------------------------- + # GESTIÓN DE DATOS EN MEMORIA + # ------------------------------------------------------------- + + def get_emisoras(self): + """Retorna la lista de emisoras cargadas en memoria.""" + return self.emisoras_cargadas + + def add_radio(self, nombre, url, pais=None, genero=None): + """ + Añade una nueva emisora a la lista en memoria y guarda el archivo. + """ + if not nombre or not url: + print("❌ El nombre y la URL son obligatorios.") + return False + + nueva_emisora = { + "nombre": nombre.strip(), + "url_stream": url.strip(), + "pais": pais.strip() if pais else None, + "genero": genero.strip() if genero else None + } + + self.emisoras_cargadas.append(nueva_emisora) + print(f"✅ [RadioManager] Emisora '{nombre}' añadida a la lista en memoria.") + + # Guarda inmediatamente + self.guardar_emisoras() + + return True \ No newline at end of file diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index 6399de7..0b646e9 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -14,8 +14,8 @@ from logica.T2.scraping import hacer_scraping from logica.T2.musicReproductor import MusicReproductor # --- Módulos de Vistas --- -# ❌ Eliminamos: from vista.central_panel.view_radio import RadioPanel -# 🔑 NUEVA IMPORTACIÓN DE VISTA MODULAR +# 🔑 IMPORTACIÓN DE LA VENTANA SECUNDARIA DEL BUSCAMINAS +from vista.ventana_buscaMinas import VentanaBuscaMinas from vista.reproductor_controller import ReproductorController from vista.config import * @@ -37,8 +37,10 @@ class PanelLateral(ttk.Frame): self.controles_musica = None self.entrada_superior = None + # 🔑 REFERENCIA A LA VENTANA NO-MODAL DEL BUSCAMINAS + self.buscaminas_window = None + # 🔑 INSTANCIA DE LÓGICA DE MÚSICA T2 - # Inicializamos el objeto de la lógica de reproducción aquí self.music_reproductor = MusicReproductor() self.configurar_estilos_locales(root) @@ -57,7 +59,7 @@ class PanelLateral(ttk.Frame): ttk.Separator(self, orient='horizontal').grid(row=4, column=0, sticky="ew", pady=(10, 0)) tk.Frame(self, height=1).grid(row=99, column=0, sticky="nsew") - # 🔑 LLAMADA AL NUEVO CONTROLADOR + # 🔑 LLAMADA AL CONTROLADOR DE MÚSICA self.crear_controles_musica() # Fila 100 # ------------------------------------------------------------- @@ -88,8 +90,9 @@ class PanelLateral(ttk.Frame): acciones_aplicaciones = [ ("Visual Code", abrir_vscode), - ("Carrera 🏁", app2_comando), - ("App3", lambda: accion_placeholder("App3")) + ("Carrera de Camellos 🏁", app2_comando), + # 🔑 VINCULACIÓN DEL BOTÓN APP3 CON LA FUNCIÓN DE LANZAMIENTO + ("Juego de Buscaminas 💣", self.manejar_app3) ] self._crear_bloque_botones(self, titulo="Aplicaciones", acciones=acciones_aplicaciones, grid_row=2) @@ -150,11 +153,35 @@ class PanelLateral(ttk.Frame): Llama al método 'manejar_inicio_carrera' del Panel Central. """ if self.panel_central: - print("Botón App2 presionado. Iniciando Carrera de Camellos en Panel Central...") + print("Botón Carrera presionado. Iniciando Carrera de Camellos en Panel Central...") self.panel_central.manejar_inicio_carrera() else: messagebox.showerror("Error", "El Panel Central no está inicializado.") + # 🔑 FUNCIÓN PARA LANZAR LA VENTANA SECUNDARIA NO-MODAL + def manejar_app3(self): + """ + Lanza la ventana secundaria (Toplevel) del Buscaminas. + Asegura que solo haya una instancia abierta a la vez. + """ + + # 1. Verificar si la ventana ya existe y está abierta + if self.buscaminas_window and self.buscaminas_window.winfo_exists(): + # Si existe, la traemos al frente y le damos foco + self.buscaminas_window.lift() + print("❌ La ventana Buscaminas ya está abierta. Trayendo al frente.") + return + + # 2. Crear y guardar la referencia de la nueva ventana + try: + # Creamos la instancia Toplevel, pasando la ventana principal (root) como parent + self.buscaminas_window = VentanaBuscaMinas(self.root) + print("✅ Ventana Buscaminas lanzada.") + + except Exception as e: + messagebox.showerror("Error de Aplicación", f"No se pudo iniciar el Buscaminas: {e}") + print(f"Error al iniciar Buscaminas: {e}") + def manejar_navegacion(self, event=None): """ Obtiene el texto de la entrada superior y llama a la función de navegación (abrir navegador externo). @@ -209,7 +236,10 @@ class PanelLateral(ttk.Frame): ttk.Label(frame_titulo, text=titulo, font=FUENTE_NEGOCIOS).pack(anchor="w", padx=5) for texto_boton, comando in acciones: - ttk.Button(frame_seccion, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5) + # 🔑 CORRECCIÓN FINAL: Todos los botones de acción usarán el estilo verde ('Green.TButton'). + style_to_use = 'Green.TButton' + + ttk.Button(frame_seccion, text=texto_boton, command=comando, style=style_to_use).pack(fill="x", pady=5) def set_panel_central_reference(self, panel_central_instance): """ diff --git a/vista/ventana_buscaMinas.py b/vista/ventana_buscaMinas.py new file mode 100644 index 0000000..a0b63f9 --- /dev/null +++ b/vista/ventana_buscaMinas.py @@ -0,0 +1,212 @@ +# Módulo: vista/ventana_buscaMinas.py + +import tkinter as tk +from tkinter import ttk, messagebox +# 🔑 Importamos la lógica del minijuego +from logica.T2.buscaMinas import BuscaMinas +# 🔑 Importamos la configuración de estilos +from vista.config import * + + +class VentanaBuscaMinas(tk.Toplevel): + """ + Ventana secundaria (tk.Toplevel) para el Buscaminas. + Es no-modal y permite interactuar con la VentanaPrincipal. + """ + + def __init__(self, parent): + # Inicializar como ventana secundaria + super().__init__(parent) + self.title("💣 Buscaminas - App3") + self.geometry("500x600") + self.resizable(False, False) + + # 1. Inicializar la lógica del juego (16x16, 40 minas) + self.game = BuscaMinas(rows=16, cols=16, mines=40) + self.buttons = {} # Almacena las referencias a los botones (celdas) + + # Variables de control de UI + self.feedback_var = tk.StringVar(value="Haz clic para empezar...") + + # 2. Configurar el layout principal + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=1) + + self.create_widgets() + self.protocol("WM_DELETE_WINDOW", self.on_close) # Manejar el cierre + + def create_widgets(self): + """Crea el marco superior de control y el marco del tablero.""" + + # --- Marco Superior de Control --- + control_frame = ttk.Frame(self, padding=10) + control_frame.grid(row=0, column=0, sticky="ew") + control_frame.grid_columnconfigure(0, weight=1) + control_frame.grid_columnconfigure(1, weight=1) + + # Botón Nuevo Juego (Usamos ttk para botones fuera del tablero) + ttk.Button(control_frame, text="🔄 Nuevo Juego", command=self.reset_game, style='Action.TButton').grid( + row=0, column=0, padx=(0, 5), sticky="w") + + # Feedback (Mensajes de éxito/derrota) + ttk.Label(control_frame, textvariable=self.feedback_var, font=FUENTE_NEGOCIOS).grid( + row=0, column=1, padx=(5, 0), sticky="e") + + # --- Marco del Tablero --- + self.board_frame = ttk.Frame(self, padding=5, style='TFrame') + self.board_frame.grid(row=1, column=0, sticky="nsew") + + self.draw_board() + + def draw_board(self): + """Crea la matriz de botones que representan las celdas del buscaminas.""" + + # Limpiar y reconfigurar el frame + for widget in self.board_frame.winfo_children(): + widget.destroy() + self.buttons = {} + + # Asegurar que los botones se expandan uniformemente + for i in range(self.game.rows): + self.board_frame.grid_rowconfigure(i, weight=1) + for j in range(self.game.cols): + self.board_frame.grid_columnconfigure(j, weight=1) + + for r in range(self.game.rows): + for c in range(self.game.cols): + # USANDO tk.Button ESTÁNDAR para la configuración dinámica de colores + btn = tk.Button( + self.board_frame, + text="", + width=2, + height=1, + relief=tk.RAISED, + background='gray70', + activebackground='gray50', + # Clic Izquierdo (Button-1): revelar (usando 'command') + command=lambda row=r, col=c: self.handle_click(row, col) + ) + btn.grid(row=r, column=c, sticky="nsew", padx=1, pady=1) + self.buttons[(r, c)] = btn + + # VINCULAR CLIC DERECHO (Button-3) a handle_flag + btn.bind("", lambda event, row=r, col=c: self.handle_flag(row, col)) + + def handle_click(self, r, c): + """Maneja el clic Izquierdo para revelar.""" + + if self.game.game_over: + return + + message, game_over = self.game.reveal_cell(r, c) + + # 1. Actualizar el tablero con el nuevo estado + self.update_ui_board() + + # 2. Manejar el estado final del juego + if game_over: + self.feedback_var.set(message) + self.show_end_game() + + else: + self.feedback_var.set("¡Cuidado! Minando...") + # Mostrar mensaje si la lógica impidió la revelación (ej: por bandera) + if message: + self.feedback_var.set(message) + + def handle_flag(self, r, c): + """ + Maneja el clic Derecho para colocar/quitar la bandera. + """ + if self.game.game_over: + return + + # Llama a la lógica para alternar la bandera + if self.game.toggle_flag(r, c): + self.update_ui_board() # Solo actualizamos si el estado de la bandera cambió + + def update_ui_board(self): + """Recorre el estado lógico y actualiza el texto, color y estado de los botones.""" + state = self.game.get_board_state() + revealed_bg = 'lightgray' + + for r in range(self.game.rows): + for c in range(self.game.cols): + text = state[r][c] + btn = self.buttons[(r, c)] + + # Estado por defecto (oculto, levantado) + btn.config(text="", background='gray70', foreground='black', relief=tk.RAISED, state=tk.NORMAL) + + if text == 'F': + # Bandera (juego en curso) + btn.config(text="🚩", foreground='blue', relief=tk.RAISED, state=tk.NORMAL) + + # 🔑 NUEVO: Manejo de estado final de banderas + elif self.game.game_over: + btn.config(state=tk.DISABLED) # Deshabilitar todos los botones + + if text == 'F+': + # Bandera Correcta (Mina marcada) + btn.config(text="✅", background='green', foreground='white', relief=tk.RAISED) + elif text == 'F-': + # Bandera Incorrecta (Celda segura marcada) + btn.config(text="❌", background='orange', foreground='white', relief=tk.RAISED) + elif text == 'M': + # Mina sin marcar (se revela al final) + btn.config(text="💣", background='black', foreground='white', relief=tk.SUNKEN) + # El resto de celdas se manejan a continuación si están reveladas. + + if self.game.revealed[r][c]: + # Celda revelada + btn.config(relief=tk.SUNKEN, state=tk.DISABLED) + + if text == 'M': + # Mina clickeada (rojo, mina detonadora) + btn.config(text="💣", background='red', foreground='white') + + elif text != '0': + # Número + btn.config( + text=text, + background=revealed_bg, + foreground=self._get_number_color(int(text)) + ) + else: + # Cero + btn.config(text="", background=revealed_bg) + + def show_end_game(self): + """Muestra el estado final, revela el tablero completo y deshabilita clics.""" + + self.update_ui_board() + + # Asegurar el deshabilitado final (redundante pero seguro) + for btn in self.buttons.values(): + btn.config(state=tk.DISABLED) + + def reset_game(self): + """Reinicia el juego lógico y recrea el tablero visual.""" + self.game.setup_board() + self.feedback_var.set("¡A jugar! Encontrando minas en 16x16...") + self.draw_board() + self.update_ui_board() + + def _get_number_color(self, number): + """Retorna un color basado en el número de la mina.""" + colors = { + 1: 'blue', + 2: 'green', + 3: 'red', + 4: 'purple', + 5: 'maroon', + 6: 'turquoise', + 7: 'black', + 8: 'gray' + } + return colors.get(number, 'black') + + def on_close(self): + """Maneja el evento de cierre de la ventana Toplevel.""" + print("[Minigame] Cerrando ventana Buscaminas.") + self.destroy() \ No newline at end of file