feat(vista): Refactoriza ventana principal (main)
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.
This commit is contained in:
parent
731050242a
commit
71ff931b5c
|
|
@ -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)~~
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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("<Button-3>", 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()
|
||||
Loading…
Reference in New Issue