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
|
bs4 => pip install bs4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
requests => pip install requests
|
requests => pip install requests
|
||||||
|
|
||||||
|
|
||||||
## Como Ejecutar ##
|
## Como Ejecutar ##
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Desde la carpeta anterior
|
> Desde la carpeta anterior
|
||||||
|
|
@ -46,6 +45,6 @@ python -m ProyectoGlobal
|
||||||
|
|
||||||
4. ~~Scraping~~
|
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)~~
|
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
|
from logica.T2.musicReproductor import MusicReproductor
|
||||||
|
|
||||||
# --- Módulos de Vistas ---
|
# --- Módulos de Vistas ---
|
||||||
# ❌ Eliminamos: from vista.central_panel.view_radio import RadioPanel
|
# 🔑 IMPORTACIÓN DE LA VENTANA SECUNDARIA DEL BUSCAMINAS
|
||||||
# 🔑 NUEVA IMPORTACIÓN DE VISTA MODULAR
|
from vista.ventana_buscaMinas import VentanaBuscaMinas
|
||||||
from vista.reproductor_controller import ReproductorController
|
from vista.reproductor_controller import ReproductorController
|
||||||
from vista.config import *
|
from vista.config import *
|
||||||
|
|
||||||
|
|
@ -37,8 +37,10 @@ class PanelLateral(ttk.Frame):
|
||||||
self.controles_musica = None
|
self.controles_musica = None
|
||||||
self.entrada_superior = 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
|
# 🔑 INSTANCIA DE LÓGICA DE MÚSICA T2
|
||||||
# Inicializamos el objeto de la lógica de reproducción aquí
|
|
||||||
self.music_reproductor = MusicReproductor()
|
self.music_reproductor = MusicReproductor()
|
||||||
|
|
||||||
self.configurar_estilos_locales(root)
|
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))
|
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")
|
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
|
self.crear_controles_musica() # Fila 100
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -88,8 +90,9 @@ class PanelLateral(ttk.Frame):
|
||||||
|
|
||||||
acciones_aplicaciones = [
|
acciones_aplicaciones = [
|
||||||
("Visual Code", abrir_vscode),
|
("Visual Code", abrir_vscode),
|
||||||
("Carrera 🏁", app2_comando),
|
("Carrera de Camellos 🏁", app2_comando),
|
||||||
("App3", lambda: accion_placeholder("App3"))
|
# 🔑 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)
|
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.
|
Llama al método 'manejar_inicio_carrera' del Panel Central.
|
||||||
"""
|
"""
|
||||||
if self.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()
|
self.panel_central.manejar_inicio_carrera()
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("Error", "El Panel Central no está inicializado.")
|
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):
|
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).
|
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)
|
ttk.Label(frame_titulo, text=titulo, font=FUENTE_NEGOCIOS).pack(anchor="w", padx=5)
|
||||||
|
|
||||||
for texto_boton, comando in acciones:
|
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):
|
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