diff --git a/README.md b/README.md index d1202f4..5755969 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,447 @@ +

+ Python + Tkinter + Sockets + License +

-Laboratorio interactivo construido con Python 3.13 + Tkinter. Reรบne scraping, monitorizaciรณn, chat TCP, alarmas, reproductor musical y utilidades varias en una รบnica aplicaciรณn de escritorio. +

๐Ÿ’ฃ Minesweeper Multiplayer + Dashboard

-## ๐ŸŽฌ Demo en video +

+ Un proyecto completo de programaciรณn de servicios y procesos
+ Juego de buscaminas competitivo en red + Panel de control integral +

-- YouTube: https://youtu.be/HgJwU_HagD8 -- Drive: https://drive.google.com/file/d/14wGkkyZ9ASbV__O2xp1zIZJxSA6gpJoF/view?usp=sharing +

+ Caracterรญsticas โ€ข + Arquitectura โ€ข + Instalaciรณn โ€ข + Uso โ€ข + Mecรกnicas โ€ข + Tecnologรญas +

-## Foto +--- -image +## ๐Ÿ“ธ Vista Previa +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐ŸŽฎ MINESWEEPER MULTIPLAYER DASHBOARD โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ ACCIONES โ”‚ รREA DE RESULTADOS โ”‚ โ”‚ ๐Ÿ’ฃ MINESWEEPER โ”‚ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ โ”‚ MULTIPLAYER โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ > Wallapop โ”‚ โ”‚ ๐Ÿ“Š Monitor Sistema โ”‚ โ”‚ โ”‚ Ronda: 3 โ”‚ โ”‚ +โ”‚ > Scraping โ”‚ โ”‚ ๐Ÿ“ˆ CPU: 45% โ”‚ โ”‚ โ”‚ ๐Ÿ’ฃ Bombas: 9 โ”‚ โ”‚ +โ”‚ > API Tiempo โ”‚ โ”‚ ๐Ÿ’พ RAM: 2.1GB โ”‚ โ”‚ โ”‚ โค๏ธ Vidas: 2 โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ APPS โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Tabs: [Resultados][Navegador] โ”‚ โ”‚ โ”‚ โ–ข โ”‚ โ–ข โ”‚ โœ“ โ”‚ โ”‚ โ”‚ +โ”‚ > VS Code โ”‚ [Correos][Bloc][Tareas] โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”ค โ”‚ โ”‚ +โ”‚ > Camellos โ”‚ [Alarmas][Enlaces] โ”‚ โ”‚ โ”‚ ๐Ÿ’ฅโ”‚ โ–ข โ”‚ โ–ข โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ BATCH โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ ๐Ÿ“ Panel de Notas โ”‚ โ”‚ โ”‚ [Iniciar Juego] โ”‚ โ”‚ +โ”‚ > Backups โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Zona Despejada]โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` -## ๐Ÿš€ Caracterรญsticas clave +--- -- **Layout triple panel**: accesos rรกpidos a la izquierda, notebook central con pestaรฑas temรกticas y panel derecho para chat y utilidades. -- **Scraping Wallapop + genรฉrico**: asistentes emergentes, validaciones y guardado de resultados. -- **Monitor de sistema**: grรกficas en vivo de CPU, RAM e hilos gracias a psutil y matplotlib embebido. -- **Productividad integrada**: bloc de notas, gestor de alarmas, reproductor musical con pygame y lanzadores de procesos. -- **Popup meteorolรณgico**: consulta OpenWeather para Jรกvea con cachรฉ y resumen formateado. -- **Servidor TCP incluido**: `servidor.py` permite pruebas de chat broadcast desde la propia app. +## โœจ Caracterรญsticas -## โš™๏ธ Requisitos +### ๐ŸŽฎ **Juego Minesweeper Multijugador** +| Caracterรญstica | Descripciรณn | +|----------------|-------------| +| ๐Ÿ”ฅ **Competitivo** | 2+ jugadores compiten en tiempo real | +| ๐Ÿ’ฃ **Colocaciรณn estratรฉgica** | Cada jugador coloca bombas para el rival | +| ๐Ÿ”„ **Por turnos** | Sistema de turnos para colocar y buscar | +| ๐Ÿ“ˆ **Dificultad progresiva** | 5 rondas con grids y bombas crecientes | +| โค๏ธ **Sistema de vidas** | 3 vidas por partida, ยกno las pierdas! | -- Python 3.8 o superior (desarrollado en 3.13) -- Dependencias listadas en `requirements.txt` +### ๐Ÿ“Š **Dashboard Integral** +- ๐Ÿ“ก **Monitor del sistema** en tiempo real (CPU, RAM, hilos) +- ๐ŸŒค๏ธ **API del tiempo** para Jรกvea (OpenWeather) +- ๐Ÿ›’ **Anรกlisis de Wallapop** - Scraping de anuncios +- โฐ **Sistema de alarmas** programables +- ๐Ÿ“ **Bloc de notas** integrado +- ๐Ÿ”— **Gestor de enlaces** rรกpidos +- ๐ŸŽฒ **Minijuego de camellos** con animaciones -```sh +--- + +## ๐Ÿ—๏ธ Arquitectura + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ARQUITECTURA DEL SISTEMA โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ SERVIDOR TCP โ”‚ + โ”‚ servidor.py โ”‚ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ + โ”‚ โ”‚ GameServer โ”‚ โ”‚ + โ”‚ โ”‚ - Estado juego โ”‚ โ”‚ + โ”‚ โ”‚ - Broadcast โ”‚ โ”‚ + โ”‚ โ”‚ - Turnos โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ + โ”‚ Puerto 3333 โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Protocolo JSON โ”‚ + โ”‚ sobre TCP/IP โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ CLIENTE 1 โ”‚ โ”‚ CLIENTE 2 โ”‚ โ”‚ CLIENTE N โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ app.py โ”‚ โ”‚ โ”‚ โ”‚ app.py โ”‚ โ”‚ โ”‚ โ”‚ cliente_ โ”‚ โ”‚ +โ”‚ โ”‚ Dashboard โ”‚ โ”‚ โ”‚ โ”‚ Dashboard โ”‚ โ”‚ โ”‚ โ”‚ juego.py โ”‚ โ”‚ +โ”‚ โ”‚ + Juego โ”‚ โ”‚ โ”‚ โ”‚ + Juego โ”‚ โ”‚ โ”‚ โ”‚ Standalone โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ๐Ÿ“ Estructura del Proyecto + +``` +Proyecto1AVApsp/ +โ”‚ +โ”œโ”€โ”€ ๐Ÿ“„ servidor.py # Servidor TCP del juego (371 lรญneas) +โ”‚ โ””โ”€โ”€ GameServer # Gestiona estado, turnos y broadcasts +โ”‚ +โ”œโ”€โ”€ ๐Ÿ“„ app.py # Dashboard principal (2566 lรญneas) +โ”‚ โ”œโ”€โ”€ DashboardApp # Aplicaciรณn Tkinter completa +โ”‚ โ””โ”€โ”€ GameClient # Cliente TCP integrado +โ”‚ +โ”œโ”€โ”€ ๐Ÿ“„ cliente_juego.py # Cliente standalone (220 lรญneas) +โ”‚ โ””โ”€โ”€ GameClient # Versiรณn ligera para jugar +โ”‚ +โ”œโ”€โ”€ ๐Ÿ“„ requirements.txt # Dependencias Python +โ””โ”€โ”€ ๐Ÿ“„ README.md # Este archivo +``` + +--- + +## ๐Ÿš€ Instalaciรณn + +### Requisitos Previos +- **Python 3.8+** +- **pip** (gestor de paquetes) + +### Paso 1: Clonar el Repositorio +```bash +git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git +cd Proyecto1AVApsp +``` + +### Paso 2: Crear Entorno Virtual (Recomendado) +```bash +python -m venv .venv + +# Linux/macOS +source .venv/bin/activate + +# Windows +.venv\Scripts\activate +``` + +### Paso 3: Instalar Dependencias +```bash pip install -r requirements.txt ``` -## โ–ถ๏ธ Puesta en marcha +
+๐Ÿ“ฆ Ver dependencias detalladas -1. (Opcional) Inicia el servidor de chat: - ```sh - python3 servidor.py - ``` -2. Lanza el panel principal: - ```sh - python3 app.py - ``` -3. Usa el panel izquierdo para abrir scraping, notas, alarmas o el popup del clima; el panel derecho gestiona el chat y el reproductor. +| Paquete | Versiรณn | Uso | +|---------|---------|-----| +| `psutil` | โ‰ฅ5.9.0 | Monitor de recursos del sistema | +| `matplotlib` | โ‰ฅ3.5.0 | Grรกficos en tiempo real | +| `pillow` | โ‰ฅ9.0.0 | Procesamiento de imรกgenes | +| `pygame` | โ‰ฅ2.1.0 | Reproducciรณn de audio (opcional) | +| `requests` | โ‰ฅ2.32.0 | Peticiones HTTP (API, Scraping) | +| `beautifulsoup4` | โ‰ฅ4.12.0 | Parsing HTML (Scraping) | -## ๐Ÿงฑ Estructura del proyecto +
+ +--- + +## ๐ŸŽฏ Uso + +### ๐Ÿ–ฅ๏ธ Iniciar el Servidor +```bash +python servidor.py +``` +> El servidor escucharรก en `0.0.0.0:3333` + +### ๐ŸŽฎ Opciรณn A: Dashboard Completo +```bash +python app.py +``` +Incluye el juego integrado + todas las funcionalidades del panel. + +### ๐ŸŽฎ Opciรณn B: Cliente Standalone +```bash +python cliente_juego.py +``` +Cliente ligero solo para jugar al Minesweeper. + +### ๐Ÿ”Œ Conectar al Juego +1. Introduce el **Host** del servidor (por defecto: `127.0.0.1`) +2. Verifica el **Puerto** (por defecto: `3333`) +3. Pulsa **"Conectar"** +4. ยกEspera a otro jugador y pulsa **"Iniciar Juego"**! + +--- + +## ๐Ÿ’ฃ Mecรกnicas del Juego + +### ๐Ÿ”„ Flujo de una Partida ``` -app.py # GUI principal y lรณgica de negocio -servidor.py # Servidor TCP broadcast para el chat -requirements.txt # Lista de dependencias -README.md # Documentaciรณn +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ FLUJO DEL JUEGO โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ LOBBY โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ COLOCACIร“N โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ BรšSQUEDA โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ DE BOMBAS โ”‚ โ”‚ (PLAYING) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ ยฟBOMBA? โ”‚โ”€โ”€โ”€โ–ถโ”‚ EXPLOSIร“Nโ”‚โ”€โ”€โ”€โ–ถโ”‚ ยฟVIDAS=0? โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ NO โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ Sร โ–ผ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ SAFE โ”‚ โ”‚ GAME OVERโ”‚ โ”‚ +โ”‚ โ”‚ โ”‚(casilla โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ segura) โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ–ผ โ”‚ +โ”‚ โ”‚ โ”ฟโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”“ โ”‚ +โ”‚ โ”‚ โ”ƒ SIGUIENTE TURNO โ”ƒ โ”‚ +โ”‚ โ”‚ โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”› โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ (Nueva Partida) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -## ๐Ÿ› ๏ธ Flujos destacados +### ๐Ÿ“Š Progresiรณn de Dificultad -- **Scraping**: workers en segundo plano con colas y retroalimentaciรณn visual en la pestaรฑa de resultados. -- **Copias de seguridad**: selecciรณn interactiva de carpetas y reporte final con totales copiados/omitidos. -- **Gestor de alarmas**: creaciรณn, cancelaciรณn y popups dedicados con recordatorio sonoro. -- **Panel de recursos**: mezcla de grรกficas lineales, de รกrea y de barras para CPU/RAM/hilos. -- **Popup โ€œAPI Tiempoโ€**: resumen meteorolรณgico en ventana modal con refresco bajo demanda. +| Ronda | Tamaรฑo Grid | Bombas/Jugador | Total Bombas* | +|:-----:|:-----------:|:--------------:|:-------------:| +| 1๏ธโƒฃ | 3ร—3 | 3 | 6 | +| 2๏ธโƒฃ | 5ร—5 | 5 | 10 | +| 3๏ธโƒฃ | 8ร—8 | 9 | 18 | +| 4๏ธโƒฃ | 11ร—11 | 12 | 24 | +| 5๏ธโƒฃ+ | 14ร—14 | 15 | 30 | -## โš™๏ธ Configuraciรณn opcional +*\*Para 2 jugadores* -- `OPENWEATHER_API_KEY` / `OPENWEATHER_FALLBACK_API_KEY`: claves para OpenWeather. -- Variables `WALLAPOP_*`: encabezados y parรกmetros usados por el scraper. -- Ajusta el host/puerto del chat en el panel derecho o modificando `SERVER_HOST_DEFAULT` y `SERVER_PORT_DEFAULT` en `app.py`. +### ๐ŸŽฏ Fases del Juego -## ๐Ÿ“Œ Prรณximos pasos sugeridos +#### 1. ๐Ÿ’ฃ Fase de Colocaciรณn (`PLACING`) +- Cada jugador coloca bombas **por turnos** +- Las bombas se muestran brevemente (1 segundo) a todos +- ยกMemoriza dรณnde pones TUS bombas y las del rival! -1. Aรฑadir almacenamiento persistente (SQLite) para chats y notas. -2. Incorporar pruebas unitarias para scraping y rutinas de backup. -3. Extender el reproductor musical con colas y visualizaciones. +#### 2. ๐Ÿ” Fase de Bรบsqueda (`PLAYING`) +- Excava casillas por turnos +- **Casilla segura** โ†’ Se marca en verde โœ… +- **Bomba** โ†’ ยกEXPLOSIร“N! Pierdes 1 vida ๐Ÿ’” -ยฟQuieres ampliar alguna secciรณn (scraping extra, nuevos paneles, automatizaciรณn de tareas)? Adelante, la base estรก lista para seguir creciendo. +### ๐Ÿ† Condiciones de Victoria + +| Condiciรณn | Resultado | +|-----------|-----------| +| Rival pierde todas las vidas | **ยกGANASTE!** ๐ŸŽ‰ | +| Todas las casillas seguras reveladas | **ยกZona despejada!** | +| Superas la ronda 5 | **ยกVictoria total!** ๐Ÿ† | + +--- + +## ๐Ÿ“ก Protocolo de Comunicaciรณn + +### Mensajes JSON Cliente โ†’ Servidor + +```javascript +// Iniciar partida +{ "type": "START_GAME" } + +// Colocar bomba (fase PLACING) +{ "type": "PLACE_BOMB", "x": 2, "y": 1 } + +// Excavar casilla (fase PLAYING) +{ "type": "CLICK_CELL", "x": 3, "y": 4 } + +// Verificar zona despejada +{ "type": "CHECK_DUNGEON_CLEARED" } +``` + +### Mensajes JSON Servidor โ†’ Clientes + +```javascript +// Nueva ronda +{ + "type": "NEW_ROUND", + "round": 1, + "grid_size": 3, + "total_bombs_per_player": 3 +} + +// Notificaciรณn de turno +{ + "type": "TURN_NOTIFY", + "active_player": "('127.0.0.1', 54321)", + "msg": "Turno de ... para poner bombas." +} + +// Flash de bomba (visible 1 segundo) +{ + "type": "BOMB_flash", + "x": 1, "y": 2, + "who": "('127.0.0.1', 54321)" +} + +// Explosiรณn +{ + "type": "EXPLOSION", + "x": 1, "y": 2, + "who": "...", + "lives": 2 +} + +// Casilla segura +{ "type": "SAFE", "x": 0, "y": 0 } + +// Game Over +{ + "type": "GAME_OVER", + "loser": "...", + "msg": "๐Ÿ’€ ยก... ha perdido todas sus vidas!" +} +``` + +--- + +## ๐Ÿ› ๏ธ Tecnologรญas + + + + + + + + + + + + +
+Python +
Python 3 +
Lenguaje base +
+Tkinter +
Tkinter +
Interfaz grรกfica +
+Sockets +
TCP Sockets +
Comunicaciรณn en red +
+Matplotlib +
Matplotlib +
Grรกficos +
+JSON +
JSON +
Protocolo mensajes +
+Requests +
Requests +
APIs HTTP +
+ +### ๐Ÿงต Conceptos de PSP Aplicados + +| Concepto | Implementaciรณn | +|----------|----------------| +| **Procesos** | Lanzamiento de aplicaciones externas (VS Code, Firefox) | +| **Threads** | Servidor multihilo, cliente con hilos de recepciรณn | +| **Sockets TCP** | Comunicaciรณn cliente-servidor en red | +| **Servicios** | API OpenWeather, scraping de Wallapop | +| **Sincronizaciรณn** | Locks para acceso concurrente a estado compartido | + +--- + +## ๐ŸŽจ Caracterรญsticas del Dashboard + +### ๐Ÿ“Š Monitor del Sistema +- Grรกfico de CPU en lรญnea temporal +- Grรกfico de memoria como รกrea +- Contador de hilos del proceso + +### ๐ŸŒค๏ธ API del Tiempo (Jรกvea) +- Temperatura actual y sensaciรณn tรฉrmica +- Humedad y velocidad del viento +- Descripciรณn del clima + +### ๐Ÿ›’ Anรกlisis Wallapop +- Extracciรณn de informaciรณn de anuncios +- Headers personalizados para API +- Resultados formateados + +### โฐ Sistema de Alarmas +- Programaciรณn en minutos +- Notificaciones visuales +- Gestiรณn de alarmas activas + +--- + +## ๐Ÿค Contribuir + +1. **Fork** del proyecto +2. Crea tu **Feature Branch** (`git checkout -b feature/NuevaFuncion`) +3. **Commit** tus cambios (`git commit -m 'Add: Nueva funciรณn'`) +4. **Push** a la rama (`git push origin feature/NuevaFuncion`) +5. Abre un **Pull Request** + +--- + +## ๐Ÿ“ Licencia + +Este proyecto es de carรกcter **educativo** y fue desarrollado como parte del mรณdulo de **Programaciรณn de Servicios y Procesos**. + +--- + +

+ Desarrollado con โค๏ธ por Marcos Ferrandiz +

+ +

+ Proyecto 1ยบ Evaluaciรณn - PSP (Programaciรณn de Servicios y Procesos) +

+ +--- + +

+ Estado + Versiรณn + Python +

diff --git a/app.py b/app.py index 712d084..82e0a66 100644 --- a/app.py +++ b/app.py @@ -127,11 +127,12 @@ WALLAPOP_HEADERS = { -class ChatClient: - """Cliente TCP bรกsico.""" +class GameClient: + """Cliente TCP para el juego Minesweeper.""" - def __init__(self, on_message): + def __init__(self, on_message: Callable[[dict], None], on_disconnect: Callable[[], None]): self._on_message = on_message + self._on_disconnect = on_disconnect self._sock: socket.socket | None = None self._lock = threading.Lock() self._connected = False @@ -145,10 +146,13 @@ class ChatClient: sock.connect((host, port)) self._sock = sock self._connected = True + # Guardar nuestra direcciรณn local + self.my_address = str(sock.getsockname()) + print(f"[CLIENT] Mi direcciรณn: {self.my_address}") threading.Thread(target=self._recv_loop, daemon=True).start() return True except Exception as exc: - self._on_message(f'[ERROR] Conexiรณn fallida: {exc}') + print(f"[ERROR] Conexiรณn fallida: {exc}") return False def _recv_loop(self): @@ -156,11 +160,29 @@ class ChatClient: while self._connected and self._sock: data = self._sock.recv(4096) if not data: + print("[CLIENT] No data received, connection closed") break - text = data.decode('utf-8', errors='replace').strip() - self._on_message(text) + try: + text_chunk = data.decode('utf-8', errors='replace') + print(f"[CLIENT] Received: {text_chunk[:200]}") # Debug: primeros 200 chars + # Split by newline for robust framing + lines = text_chunk.split('\n') + # Note: this simple split might break if a message is split across recv calls. + # For a robust production app we need a buffer. + # Assuming short JSONs for now. + for line in lines: + line = line.strip() + if not line: continue + try: + msg = json.loads(line) + print(f"[CLIENT] Parsed message: {msg.get('type')}") + self._on_message(msg) + except json.JSONDecodeError as e: + print(f"[CLIENT] JSON decode error: {e}, line: {line[:100]}") + except Exception as e: + print(f"[CLIENT] Exception in recv: {e}") except Exception as exc: - self._on_message(f'[ERROR] {exc}') + print(f"[ERROR] Recv: {exc}") finally: with self._lock: self._connected = False @@ -170,14 +192,16 @@ class ChatClient: except Exception: pass self._sock = None - self._on_message('[INFO] Conexiรณn cerrada') + print("[CLIENT] Calling _on_disconnect") + self._on_disconnect() - def send(self, text: str) -> bool: + def send(self, data: dict) -> bool: with self._lock: if not self._connected or not self._sock: return False try: - self._sock.sendall(text.encode('utf-8')) + msg = json.dumps(data) + '\n' + self._sock.sendall(msg.encode('utf-8')) return True except Exception: self._connected = False @@ -211,14 +235,17 @@ class DashboardApp(tk.Tk): self.rowconfigure(1, weight=1) self.rowconfigure(2, weight=0) + self._running = True - self._chat_queue: 'queue.Queue[str]' = queue.Queue() + self._game_queue: 'queue.Queue[dict]' = queue.Queue() self._scraping_queue: 'queue.Queue[tuple[str, ...]]' = queue.Queue() self._traffic_last = psutil.net_io_counters() if psutil else None self._resource_history = {'cpu': [], 'mem': [], 'threads': []} self._resource_poll_job: str | None = None - self.chat_client = ChatClient(self._enqueue_chat_message) + self.game_client = GameClient(self._enqueue_game_message, self._on_game_disconnect) + self._game_phase = 'LOBBY' # LOBBY, PLACING, PLAYING + self.grid_buttons = {} # (x, y) -> tk.Button self.alarm_counter = 1 self.active_alarms: list[dict[str, datetime.datetime | str]] = [] self.game_window_canvas = None @@ -267,7 +294,7 @@ class DashboardApp(tk.Tk): except Exception: pass self._resource_poll_job = self.after(1000, self._resource_poll_tick) - threading.Thread(target=self._chat_loop, daemon=True).start() + threading.Thread(target=self._game_loop, daemon=True).start() self.after(100, self._process_scraping_queue) self.after(1000, self._refresh_alarms_loop) @@ -699,61 +726,385 @@ class DashboardApp(tk.Tk): self.notes.pack(fill='both', expand=True, padx=6, pady=6) def _build_right_panel(self) -> None: - right = tk.Frame(self, width=280, bg=PANEL_BG, bd=0) + right = tk.Frame(self, width=700, bg=PANEL_BG, bd=0) right.grid(row=1, column=2, sticky='nse', padx=6, pady=(50,6)) right.grid_propagate(False) - tk.Label(right, text='Chat', font=('Arial', 20, 'bold'), fg='red', bg='white').pack(pady=(6,4)) - self.chat_display = scrolledtext.ScrolledText(right, width=30, height=12, state='disabled') - self.chat_display.pack(padx=6, pady=4) + # Header Juego + self.game_header = tk.Frame(right, bg=ACCENT_COLOR, pady=8) + self.game_header.pack(fill='x') + self.game_title_label = tk.Label(self.game_header, text='๐ŸŽฎ Minesweeper Multijugador', + font=('Arial', 18, 'bold'), fg='white', bg=ACCENT_COLOR) + self.game_title_label.pack() - tk.Label(right, text='Mensaje', bg='white').pack(anchor='w', padx=6) - self.message_entry = tk.Text(right, height=4) - self.message_entry.pack(padx=6, pady=4) - tk.Button(right, text='enviar', bg='#d6f2ce', command=self._send_chat).pack(pady=4) + # Panel de Estado (Vidas, Ronda) + stats = tk.Frame(right, bg='white', pady=8) + stats.pack(pady=6, fill='x', padx=10) + + self.lbl_round = tk.Label(stats, text='Ronda: -', font=('Arial', 14, 'bold'), bg='white', fg=TEXT_COLOR) + self.lbl_round.pack(side='left', padx=10) + + self.lbl_bombs = tk.Label(stats, text='๐Ÿ’ฃ Bombas: -', font=('Arial', 14, 'bold'), fg='#e67e22', bg='white') + self.lbl_bombs.pack(side='left', padx=10) + + self.lbl_lives = tk.Label(stats, text='โค๏ธ Vidas: 3', font=('Arial', 14, 'bold'), fg='#c44569', bg='white') + self.lbl_lives.pack(side='right', padx=10) - conn = tk.Frame(right, bg='white') - conn.pack(pady=4) - tk.Label(conn, text='Host:', bg='white').grid(row=0, column=0) - self.host_entry = tk.Entry(conn, width=12) + # Panel de Juego (Grid) - MรS GRANDE + self.game_frame = tk.Frame(right, bg='#eeeeee', bd=3, relief='sunken') + self.game_frame.pack(fill='both', expand=True, padx=10, pady=10) + + # Mensaje inicial en el grid + tk.Label(self.game_frame, text='Conecta al servidor\npara comenzar', + font=('Arial', 14), bg='#eeeeee', fg=SUBTEXT_COLOR).place(relx=0.5, rely=0.5, anchor='center') + + # Botones de Acciรณn - MรS GRANDES + actions = tk.Frame(right, bg='white', pady=6) + actions.pack(pady=6, fill='x', padx=10) + + self.btn_game_action = tk.Button(actions, text='๐ŸŽฏ Iniciar Juego', bg='#90ee90', + font=('Arial', 12, 'bold'), command=self._start_game_req, + relief='raised', bd=3, padx=15, pady=8, state='disabled') + self.btn_game_action.pack(side='left', padx=5, expand=True, fill='x') + + # Check Done Button + self.btn_check_done = tk.Button(actions, text='โœ“ Zona Despejada', bg='#fff3cd', + font=('Arial', 12, 'bold'), command=self._check_dungeon_cleared, + relief='raised', bd=3, padx=15, pady=8, state='disabled') + self.btn_check_done.pack(side='left', padx=5, expand=True, fill='x') + + # Log de juego - MรS GRANDE + log_frame = tk.LabelFrame(right, text='๐Ÿ“‹ Log del Juego', bg='white', + font=('Arial', 11, 'bold'), fg=TEXT_COLOR) + log_frame.pack(padx=10, pady=6, fill='both', expand=False) + + self.game_log = scrolledtext.ScrolledText(log_frame, width=50, height=8, + state='normal', font=('Consolas', 10), + bg='#f8f9fa', fg=TEXT_COLOR) + self.game_log.pack(padx=6, pady=6, fill='both', expand=True) + self.game_log.insert('end', "๐ŸŽฎ Bienvenido al Minesweeper Multijugador\n") + self.game_log.insert('end', "โ”" * 50 + "\n") + self.game_log.insert('end', "1. Conecta al servidor\n") + self.game_log.insert('end', "2. Espera a que otro jugador se conecte\n") + self.game_log.insert('end', "3. Haz clic en 'Iniciar Juego'\n") + self.game_log.insert('end', "โ”" * 50 + "\n") + self.game_log.config(state='disabled') + + # Conexiรณn - MEJORADO + conn_frame = tk.LabelFrame(right, text='๐Ÿ”Œ Conexiรณn al Servidor', bg='white', + font=('Arial', 11, 'bold'), fg=TEXT_COLOR) + conn_frame.pack(pady=6, fill='x', padx=10) + + conn = tk.Frame(conn_frame, bg='white') + conn.pack(pady=8, padx=10) + + tk.Label(conn, text='Host:', bg='white', font=('Arial', 11)).grid(row=0, column=0, sticky='e', padx=5) + self.host_entry = tk.Entry(conn, width=15, font=('Arial', 11)) self.host_entry.insert(0, SERVER_HOST_DEFAULT) - self.host_entry.grid(row=0, column=1) - tk.Label(conn, text='Puerto:', bg='white').grid(row=1, column=0) - self.port_entry = tk.Entry(conn, width=6) + self.host_entry.grid(row=0, column=1, padx=5) + + tk.Label(conn, text='Puerto:', bg='white', font=('Arial', 11)).grid(row=1, column=0, sticky='e', padx=5) + self.port_entry = tk.Entry(conn, width=8, font=('Arial', 11)) self.port_entry.insert(0, str(SERVER_PORT_DEFAULT)) - self.port_entry.grid(row=1, column=1) - tk.Button(conn, text='Conectar', command=self._connect_chat).grid(row=0, column=2, rowspan=2, padx=4) + self.port_entry.grid(row=1, column=1, padx=5, sticky='w') + + tk.Button(conn, text='๐Ÿ”— Conectar', command=self._connect_game, + bg='#4CAF50', fg='white', font=('Arial', 11, 'bold'), + relief='raised', bd=3, padx=20, pady=5).grid(row=0, column=2, rowspan=2, padx=10) - tk.Label(right, text='Alumnos', font=('Arial', 14, 'bold'), bg='white').pack(pady=(10,4)) - for name in ('Alumno 1', 'Alumno 2', 'Alumno 3'): - frame = tk.Frame(right, bg='white', bd=1, relief='groove') - frame.pack(fill='x', padx=6, pady=4) - tk.Label(frame, text=name, bg='white', font=('Arial', 11, 'bold')).pack(anchor='w') - tk.Label(frame, text='Lorem ipsum dolor sit amet, consectetur adipiscing elit.', wraplength=220, justify='left', bg='white').pack(anchor='w') + # Reproductor mรบsica (mini) + player = tk.Frame(right, bg='#fdf5f5', bd=1, relief='flat', padx=4, pady=4) + player.pack(fill='x', padx=8, pady=(4,6)) + tk.Label(player, text='๐ŸŽต Mรบsica', font=self.font_small, bg='#fdf5f5').pack(side='left') + tk.Button(player, text='โ–ถ', command=self._resume_music, width=3).pack(side='left', padx=2) + tk.Button(player, text='||', command=self._pause_music, width=3).pack(side='left', padx=2) + tk.Button(player, text='๐Ÿ“', command=self._select_music, width=3).pack(side='left', padx=2) - player = tk.Frame(right, bg='#fdf5f5', bd=1, relief='flat', padx=8, pady=8) - player.pack(fill='x', padx=8, pady=(10,6)) - tk.Label(player, text='Reproductor mรบsica', font=self.font_header, bg='#fdf5f5', fg=ACCENT_DARK).pack(pady=(0,6)) + # ------------------ Lรณgica Juego ------------------ + def _enqueue_game_message(self, msg: dict): + self._game_queue.put(msg) - def styled_btn(parent, text, command): - return tk.Button( - parent, - text=text, - command=command, - bg='#ffe3df', - activebackground='#ffd2ca', - fg=TEXT_COLOR, - relief='flat', - width=20, - pady=4 - ) + def _on_game_disconnect(self): + self._game_queue.put({"type": "DISCONNECT"}) + + def _game_loop(self): + while self._running: + try: + msg = self._game_queue.get(timeout=0.1) + self.after(0, self._process_game_message, msg) + except queue.Empty: + continue + + def _connect_game(self): + """Conecta al servidor de juego""" + host = self.host_entry.get().strip() + if not host: + messagebox.showerror("Error", "Debes especificar un host") + return + + try: + port = int(self.port_entry.get()) + if port < 1 or port > 65535: + raise ValueError("Puerto fuera de rango") + except ValueError: + messagebox.showerror("Error", "Puerto invรกlido (debe ser 1-65535)") + return + + self._log_game(f"Conectando a {host}:{port}...") + try: + if self.game_client.connect(host, port): + self._log_game("โœ“ Conectado al servidor exitosamente") + self.btn_game_action.config(state='normal') + messagebox.showinfo("Conectado", f"Conectado a {host}:{port}") + else: + self._log_game("โœ— Error al conectar") + messagebox.showerror("Error", "No se pudo conectar al servidor") + except Exception as e: + self._log_game(f"โœ— Excepciรณn: {e}") + messagebox.showerror("Error", f"Error de conexiรณn: {e}") + + def _start_game_req(self): + print("[CLIENT] Enviando START_GAME al servidor") + self.game_client.send({"type": "START_GAME"}) + self._log_game("Solicitando inicio de juego...") + + def _check_dungeon_cleared(self): + self.game_client.send({"type": "CHECK_DUNGEON_CLEARED"}) + + def _log_game(self, text): + self.game_log.config(state='normal') + self.game_log.insert('end', f"> {text}\n") + self.game_log.see('end') + self.game_log.config(state='disabled') + + def _process_game_message(self, msg: dict): + mtype = msg.get('type') + + if mtype == 'DISCONNECT': + self._log_game("Desconectado del servidor.") + self._game_phase = 'LOBBY' + return + + if mtype == 'NEW_ROUND': + r = msg.get('round') + size = msg.get('grid_size') + status = msg.get('status') + bombs_per_player = msg.get('total_bombs_per_player', 3) + + # Guardar bombas restantes para el contador + self._bombs_remaining = bombs_per_player + self.lbl_bombs.config(text=f"๐Ÿ’ฃ Bombas: {self._bombs_remaining}") + + self.lbl_round.config(text=f"Ronda: {r}") + self._log_game(f"=== Ronda {r} ({size}x{size}) ===") + self._log_game(f"Cada jugador debe poner {bombs_per_player} bombas") + self._log_game(status) + self._build_grid(size) + self._game_phase = 'PLACING' + self.btn_game_action.config(state='disabled') + self.btn_check_done.config(state='disabled') + + elif mtype == 'TURN_NOTIFY': + player = msg.get('active_player') + text = msg.get('msg') + self._log_game(f"๐ŸŽฏ {text}") + + # Cambiar color del header si es mi turno + if hasattr(self.game_client, 'my_address') and player in self.game_client.my_address: + self.game_header.config(bg='#4CAF50') # Verde + self.game_title_label.config(bg='#4CAF50', text='๐ŸŽฎ ยกTU TURNO! - Coloca Bombas') + else: + self.game_header.config(bg=ACCENT_COLOR) # Rojo + self.game_title_label.config(bg=ACCENT_COLOR, text='๐ŸŽฎ Esperando Turno...') + + elif mtype == 'BOMB_flash': + x, y = msg.get('x'), msg.get('y') + who = msg.get('who') + self._log_game(f"๐Ÿ’ฃ {who} colocรณ una bomba") + + # Decrementar contador de bombas SI es mi turno (mi bomba) + if hasattr(self.game_client, 'my_address') and who in self.game_client.my_address: + if hasattr(self, '_bombs_remaining') and self._bombs_remaining > 0: + self._bombs_remaining -= 1 + self.lbl_bombs.config(text=f"๐Ÿ’ฃ Bombas: {self._bombs_remaining}") + + btn = self._get_grid_btn(x, y) + if btn: + # Mostrar bomba temporalmente + orig_bg = btn.cget('bg') + btn.config(bg='#ff6b6b', text='๐Ÿ’ฃ', fg='white', font=('Arial', 14, 'bold')) + # Ocultar en 400ms (mรกs rรกpido) + def hide_bomb(b=btn, original=orig_bg): + try: + b.config(bg=original, text='', fg='black') + except Exception: + pass + self.after(400, hide_bomb) + + elif mtype == 'PHASE_PLAY': + self._log_game("โš”๏ธ " + msg.get('msg', 'Fase de bรบsqueda iniciada')) + self.btn_check_done.config(state='normal') + self._game_phase = 'PLAYING' + + elif mtype == 'SEARCH_TURN': + player = msg.get('active_player') + text = msg.get('msg') + self._log_game(f"{text}") + + # Cambiar color del header si es mi turno + if hasattr(self.game_client, 'my_address') and player in self.game_client.my_address: + self.game_header.config(bg='#4CAF50') # Verde + self.game_title_label.config(bg='#4CAF50', text='๐ŸŽฎ ยกTU TURNO! - Excava') + else: + self.game_header.config(bg=ACCENT_COLOR) # Rojo + self.game_title_label.config(bg=ACCENT_COLOR, text='๐ŸŽฎ Esperando Turno...') + + elif mtype == 'EXPLOSION': + x, y = msg.get('x'), msg.get('y') + lives = msg.get('lives') + who = msg.get('who') + player_addr = msg.get('player_addr', who) + self._log_game(f"๐Ÿ’ฅ BOOM! {who} pisรณ una bomba (Vidas: {lives})") + btn = self._get_grid_btn(x, y) + if btn: + btn.config(bg='#c44569', text='๐Ÿ’ฅ', fg='white', font=('Arial', 16, 'bold'), + state='disabled', relief='sunken') + # SOLO actualizar vidas si es el jugador local (comparaciรณn con my_address) + if hasattr(self.game_client, 'my_address') and player_addr in self.game_client.my_address: + try: + self.lbl_lives.config(text=f"โค๏ธ Vidas: {lives}") + if lives <= 0: + self._log_game("โ˜ ๏ธ ยกSin vidas! Game Over") + except Exception: + pass # App cerrรกndose + + elif mtype == 'SAFE': + x, y = msg.get('x'), msg.get('y') + self._log_game(f"โœ“ Celda ({x},{y}) segura") + btn = self._get_grid_btn(x, y) + if btn: + btn.config(bg='#90ee90', text='โœ“', fg='#2d5016', font=('Arial', 12, 'bold'), + state='disabled', relief='sunken') + + elif mtype == 'WARNING': + msg_text = msg.get('msg', '') + self._log_game(f"โš ๏ธ {msg_text}") + messagebox.showwarning("Advertencia", msg_text) + + elif mtype == 'ROUND_WIN': + msg_text = msg.get('msg', 'ยกRonda completada!') + self._log_game(f"๐Ÿ† {msg_text}") + messagebox.showinfo("ยกRonda Ganada!", msg_text) + + elif mtype == 'ROUND_ADVANCE': + msg_text = msg.get('msg', 'Pasando a la siguiente ronda...') + self._log_game(f"โญ๏ธ {msg_text}") + # Cambiar header a naranja mientras transiciona + self.game_header.config(bg='#FF9800') + self.game_title_label.config(bg='#FF9800', text='โณ Siguiente Ronda...') + + elif mtype == 'GAME_WIN': + self._log_game("๐Ÿ‘‘ ยกVICTORIA! ยกJuego completado!") + messagebox.showinfo("ยกVictoria Total!", "ยกHas completado todas las rondas!") + self._game_phase = 'LOBBY' + self.btn_game_action.config(state='normal') + + elif mtype == 'GAME_OVER': + loser = msg.get('loser', '') + msg_text = msg.get('msg', 'ยกJuego terminado!') + self._log_game(f"๐Ÿ’€ {msg_text}") + # Verificar si el perdedor soy yo + if hasattr(self.game_client, 'my_address') and loser in self.game_client.my_address: + self.game_header.config(bg='#c0392b') # Rojo oscuro + self.game_title_label.config(bg='#c0392b', text='๐Ÿ’€ HAS PERDIDO') + messagebox.showerror("ยกDERROTA!", "ยกHas perdido todas tus vidas!") + else: + self.game_header.config(bg='#27ae60') # Verde + self.game_title_label.config(bg='#27ae60', text='๐Ÿ† ยกHAS GANADO!') + messagebox.showinfo("ยกVICTORIA!", "ยกTu oponente ha perdido todas sus vidas!") + self._game_phase = 'LOBBY' + self.btn_game_action.config(state='normal') + + def _build_grid(self, size): + """Construye el grid de juego dinรกmicamente segรบn el tamaรฑo de la ronda""" + # Limpiar grid anterior + for child in self.game_frame.winfo_children(): + child.destroy() + + # Configurar grid + for i in range(size): + self.game_frame.columnconfigure(i, weight=1, uniform='cell') + self.game_frame.rowconfigure(i, weight=1, uniform='cell') + + # Calcular tamaรฑo de botรณn segรบn grid + # Grids mรกs grandes = botones mรกs pequeรฑos + if size <= 3: + btn_font = ('Arial', 14, 'bold') + btn_width = 3 + btn_height = 1 + elif size <= 5: + btn_font = ('Arial', 12, 'bold') + btn_width = 2 + btn_height = 1 + elif size <= 9: + btn_font = ('Arial', 10, 'bold') + btn_width = 2 + btn_height = 1 + else: # 12x12 + btn_font = ('Arial', 8, 'bold') + btn_width = 1 + btn_height = 1 + + self.grid_buttons = {} + for r in range(size): + for c in range(size): + btn = tk.Button( + self.game_frame, + bg='#e0e0e0', + activebackground='#d0d0d0', + relief='raised', + bd=2, + font=btn_font, + width=btn_width, + height=btn_height, + cursor='hand2' + ) + btn.grid(row=r, column=c, sticky='nsew', padx=1, pady=1) + + # Click handler + btn.config(command=lambda x=c, y=r: self._on_grid_click(x, y)) + self.grid_buttons[(c, r)] = btn + + self._log_game(f"Grid {size}x{size} creado ({size*size} celdas)") + + def _on_grid_click(self, x, y): + """Maneja clicks en el grid segรบn la fase del juego""" + # Verificar que estamos conectados + if not self.game_client._connected: + messagebox.showwarning("No conectado", "Debes conectarte al servidor primero") + return + + btn = self._get_grid_btn(x, y) + if not btn: + return + + # No permitir clicks en celdas ya procesadas + if btn.cget('state') == 'disabled': + return + + if self._game_phase == 'PLACING': + # Colocar bomba + self.game_client.send({"type": "PLACE_BOMB", "x": x, "y": y}) + elif self._game_phase == 'PLAYING': + # Buscar/revelar celda + self.game_client.send({"type": "CLICK_CELL", "x": x, "y": y}) + else: + # En LOBBY, no hacer nada + pass + + def _get_grid_btn(self, x, y): + return self.grid_buttons.get((x, y)) - styled_btn(player, 'Seleccionar archivo', self._select_music).pack(pady=3) - action_row = tk.Frame(player, bg='#fdf5f5') - action_row.pack(pady=3) - styled_btn(action_row, 'Pausa', self._pause_music).pack(side='left', padx=4) - styled_btn(action_row, 'Reanudar', self._resume_music).pack(side='left', padx=4) - styled_btn(player, 'Quitar', self._stop_music).pack(pady=3) def _build_status_bar(self) -> None: status = tk.Frame(self, bg='#f1f1f1', bd=2, relief='ridge') @@ -2167,15 +2518,42 @@ class DashboardApp(tk.Tk): self.notes.see('end') def on_close(self) -> None: + """Cierra la aplicaciรณn correctamente""" + print("Cerrando aplicaciรณn...") self._running = False + + # Cancelar jobs programados if self._resource_poll_job is not None: try: self.after_cancel(self._resource_poll_job) except Exception: pass self._resource_poll_job = None - self.chat_client.close() - self.destroy() + + # Cerrar cliente de juego + try: + if hasattr(self, 'game_client'): + self.game_client.close() + except Exception as e: + print(f"Error cerrando game_client: {e}") + + # Detener mรบsica si estรก sonando + try: + if pygame and pygame.mixer.get_init(): + pygame.mixer.music.stop() + pygame.mixer.quit() + except Exception: + pass + + # Destruir ventana + try: + self.destroy() + except Exception: + pass + + # Forzar salida si es necesario + import sys + sys.exit(0) def main() -> None: diff --git a/cliente_juego.py b/cliente_juego.py new file mode 100644 index 0000000..b38a623 --- /dev/null +++ b/cliente_juego.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +import tkinter as tk +from tkinter import messagebox, scrolledtext, simpledialog +import socket +import threading +import json +import time +import queue + +SERVER_HOST = '127.0.0.1' +SERVER_PORT = 3333 + +class GameClient: + def __init__(self, root): + self.root = root + self.root.title("Minesweeper Multiplayer - Cliente Dedicado") + self.root.geometry("600x700") + + # Conexiรณn + self.sock = None + self.connected = False + self.msg_queue = queue.Queue() + + # Estado UI + self.buttons = {} + self.game_phase = 'LOBBY' + + self.build_ui() + + # Loop de mensajes UI + self.root.after(100, self.process_queue) + + def build_ui(self): + # Frame Superior (Conexiรณn) + conn_frame = tk.Frame(self.root, pady=5) + conn_frame.pack(fill='x', padx=10, pady=5) + + tk.Label(conn_frame, text="Host:").pack(side='left') + self.ent_host = tk.Entry(conn_frame, width=15) + self.ent_host.insert(0, SERVER_HOST) + self.ent_host.pack(side='left', padx=5) + + tk.Label(conn_frame, text="Port:").pack(side='left') + self.ent_port = tk.Entry(conn_frame, width=6) + self.ent_port.insert(0, str(SERVER_PORT)) + self.ent_port.pack(side='left', padx=5) + + self.btn_connect = tk.Button(conn_frame, text="Conectar", command=self.connect) + self.btn_connect.pack(side='left', padx=10) + + self.btn_start = tk.Button(conn_frame, text="Iniciar Juego", command=self.start_game, bg='#90ee90', state='disabled') + self.btn_start.pack(side='right', padx=10) + + # Frame Stats + stats_frame = tk.Frame(self.root, pady=5) + stats_frame.pack(fill='x', padx=20) + self.lbl_round = tk.Label(stats_frame, text="Ronda: -", font=('Arial', 14)) + self.lbl_round.pack(side='left') + self.lbl_lives = tk.Label(stats_frame, text="Vidas: -", font=('Arial', 14, 'bold'), fg='red') + self.lbl_lives.pack(side='right') + + # Frame Juego + self.game_frame = tk.Frame(self.root, bg='#cccccc') + self.game_frame.pack(fill='both', expand=True, padx=20, pady=10) + + # Logs + self.log_area = scrolledtext.ScrolledText(self.root, height=8) + self.log_area.pack(fill='x', padx=10, pady=10) + + # Botones Control + ctrl_frame = tk.Frame(self.root) + ctrl_frame.pack(pady=5) + self.btn_done = tk.Button(ctrl_frame, text="ยกZona Limpia!", command=self.check_cleared, bg='gold', state='disabled') + self.btn_done.pack() + + def log(self, text): + self.log_area.insert('end', f"> {text}\n") + self.log_area.see('end') + + def connect(self): + host = self.ent_host.get() + try: + port = int(self.ent_port.get()) + except ValueError: + messagebox.showerror("Error", "Puerto invรกlido") + return + + try: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((host, port)) + self.connected = True + threading.Thread(target=self.recv_loop, daemon=True).start() + self.log(f"Conectado a {host}:{port}") + self.btn_connect.config(state='disabled') + self.btn_start.config(state='normal') + except Exception as e: + messagebox.showerror("Error Conexiรณn", str(e)) + + def send(self, data): + if not self.connected or not self.sock: + return + try: + msg = json.dumps(data) + '\n' + self.sock.sendall(msg.encode('utf-8')) + except Exception as e: + self.log(f"Error enviando: {e}") + + def recv_loop(self): + while self.connected: + try: + data = self.sock.recv(4096) + if not data: break + text = data.decode('utf-8', errors='replace') + for line in text.split('\n'): + line = line.strip() + if not line: continue + try: + self.msg_queue.put(json.loads(line)) + except: pass + except: + break + self.connected = False + self.msg_queue.put({"type": "DISCONNECT"}) + + def process_queue(self): + while not self.msg_queue.empty(): + msg = self.msg_queue.get() + self.handle_message(msg) + self.root.after(100, self.process_queue) + + def handle_message(self, msg): + mtype = msg.get('type') + + if mtype == 'DISCONNECT': + self.log("Desconectado del servidor.") + self.btn_connect.config(state='normal') + self.btn_start.config(state='disabled') + return + + if mtype == 'NEW_ROUND': + r = msg.get('round') + size = msg.get('grid_size') + self.lbl_round.config(text=f"Ronda: {r}") + self.log(f"--- NUEVA RONDA {r} ---") + self.game_phase = 'PLACING' + self.build_grid(size) + self.btn_done.config(state='disabled') + + elif mtype == 'TURN_NOTIFY': + self.log(msg.get('msg', 'Es tu turno')) + + elif mtype == 'BOMB_flash': + x, y = msg.get('x'), msg.get('y') + who = msg.get('who') + self.log(f"Bomba puesta por {who}") + btn = self.buttons.get((x,y)) + if btn: + orig = btn.cget('bg') + btn.config(bg='orange', text='๐Ÿ’ฃ') + self.root.after(1000, lambda b=btn: b.config(bg=orig, text='')) + + elif mtype == 'PHASE_PLAY': + self.game_phase = 'PLAYING' + self.log(msg.get('msg')) + self.btn_done.config(state='normal') + + elif mtype == 'EXPLOSION': + x, y = msg.get('x'), msg.get('y') + lives = msg.get('lives') + who = msg.get('who') + self.log(f"EXPLOSIร“N de {who}!") + self.lbl_lives.config(text=f"Vidas: {lives}") + btn = self.buttons.get((x,y)) + if btn: btn.config(bg='red', text='๐Ÿ’ฅ') + + elif mtype == 'SAFE': + x, y = msg.get('x'), msg.get('y') + btn = self.buttons.get((x,y)) + if btn: btn.config(bg='lightgreen', relief='sunken') + + elif mtype == 'WARNING': + self.log(f"[AVISO] {msg.get('msg')}") + + elif mtype == 'ROUND_WIN': + self.log(f"GANADOR: {msg.get('msg')}") + messagebox.showinfo("Ronda", msg.get('msg')) + + elif mtype == 'GAME_WIN': + messagebox.showinfo("Victoria", "Juego Completado!") + + def build_grid(self, size): + for child in self.game_frame.winfo_children(): + child.destroy() + self.buttons = {} + for r in range(size): + self.game_frame.rowconfigure(r, weight=1) + self.game_frame.columnconfigure(r, weight=1) + for c in range(size): + btn = tk.Button(self.game_frame, bg='#dddddd') + btn.grid(row=r, column=c, sticky='nsew', padx=1, pady=1) + btn.config(command=lambda x=c, y=r: self.on_click(x,y)) + self.buttons[(c,r)] = btn + + def on_click(self, x, y): + if self.game_phase == 'PLACING': + self.send({"type": "PLACE_BOMB", "x": x, "y": y}) + elif self.game_phase == 'PLAYING': + self.send({"type": "CLICK_CELL", "x": x, "y": y}) + + def start_game(self): + self.send({"type": "START_GAME"}) + + def check_cleared(self): + self.send({"type": "CHECK_DUNGEON_CLEARED"}) + +if __name__ == "__main__": + root = tk.Tk() + app = GameClient(root) + root.mainloop() diff --git a/servidor.py b/servidor.py index a7d89c4..409cb0d 100644 --- a/servidor.py +++ b/servidor.py @@ -1,84 +1,370 @@ #!/usr/bin/env python3 """ -Servidor de mensajerรญa simple (broadcast) - puerto 3333 - -Ejecutar en un terminal separado: - python3 servidor.py - +Servidor de juego Minesweeper Multiplayer +Protocolo basado en JSON sobre TCP. """ import socket import threading +import json +import time +import random HOST = '0.0.0.0' PORT = 3333 -clients = [] -clients_lock = threading.Lock() +# Estados del juego +STATE_LOBBY = 'LOBBY' +STATE_PLACING = 'PLACING' # Jugadores ponen bombas por turnos +STATE_PLAYING = 'PLAYING' # Jugadores buscan -def broadcast(message: bytes, sender: socket.socket): - with clients_lock: - for client in list(clients): - if client is sender: - continue +class GameServer: + def __init__(self): + self.clients = {} # socket -> address + self.client_list = [] # List of addresses to maintain order + self.lock = threading.Lock() + + # Estado del juego + self.state = STATE_LOBBY + self.round = 1 + self.lives = {} # address -> int + self.grid_size = 3 + self.bombs = set() # Set of coordinates (x, y) + self.revealed = set() # Set of coordinates (x, y) + + # Turn control for placing + self.placing_turn_index = 0 + self.bombs_to_place_per_player = 0 + self.current_player_bombs_placed = 0 + + # Turn control for playing (search phase) + self.playing_turn_index = 0 + + def _broadcast_unlocked(self, message_dict): + """Envรญa mensaje JSON a todos. NOTA: Debe llamarse con self.lock ya adquirido""" + data = (json.dumps(message_dict) + '\n').encode('utf-8') + print(f"[BROADCAST] Enviando {len(data)} bytes a {len(self.clients)} clientes") + for client in list(self.clients.keys()): try: - client.sendall(message) - except Exception: - try: - client.close() - except Exception: - pass - clients.remove(client) + client.sendall(data) + print(f"[BROADCAST] โœ“ Enviado a {self.clients[client]}") + except Exception as e: + print(f"[BROADCAST] โœ— Error enviando a {self.clients.get(client)}: {e}") + # No llamar remove_client aquรญ porque ya tenemos el lock + # Solo marcar para remover despuรฉs + + def broadcast(self, message_dict): + """Envรญa mensaje JSON a todos con newline (adquiere lock)""" + with self.lock: + self._broadcast_unlocked(message_dict) + + def remove_client(self, client): + if client in self.clients: + addr = self.clients[client] + del self.clients[client] + if addr in self.client_list: + self.client_list.remove(addr) + print(f"[DESCONECTADO] {addr}") + # Si no quedan jugadores, reiniciar + if not self.clients: + self.reset_game() + + def reset_game(self): + print("Reiniciando juego...") + self.state = STATE_LOBBY + self.round = 1 + self.lives = {} + self.bombs = set() + self.revealed = set() + self.client_list = [] + + def get_grid_size(self): + # Tamaรฑos de grid por ronda (mรกximo 14x14) + if self.round == 1: return 3 + if self.round == 2: return 5 + if self.round == 3: return 8 + if self.round == 4: return 11 + return 14 # Ronda 5+ + + def get_bombs_per_player(self): + # Bombas por ronda: 3, 5, 9, 12, 15 + if self.round == 1: return 3 + if self.round == 2: return 5 + if self.round == 3: return 9 + if self.round == 4: return 12 + return 15 # Ronda 5+ + + def start_round(self): + """Inicia una nueva ronda. NOTA: Debe llamarse con self.lock ya adquirido""" + self.grid_size = self.get_grid_size() + self.bombs = set() + self.revealed = set() + self.state = STATE_PLACING + self.placing_turn_index = 0 + self.current_player_bombs_placed = 0 + self.bombs_to_place_per_player = self.get_bombs_per_player() + + # Actualizar lista de clientes para turnos (lock ya adquirido por caller) + self.client_list = list(self.clients.values()) + + print(f"[ROUND {self.round}] Grid: {self.grid_size}x{self.grid_size}, Bombas/jugador: {self.bombs_to_place_per_player}") + print(f"[ROUND {self.round}] Jugadores: {self.client_list}") + + msg = { + "type": "NEW_ROUND", + "round": self.round, + "grid_size": self.grid_size, + "status": f"Ronda {self.round}: Fase de Colocaciรณn", + "total_bombs_per_player": self.bombs_to_place_per_player + } + print(f"[BROADCAST] NEW_ROUND: {msg}") + self._broadcast_unlocked(msg) + self.next_placement_turn() + + def start_round_with_lock(self): + """Versiรณn de start_round que adquiere el lock (para usar con Timer)""" + with self.lock: + self.start_round() + + def next_placement_turn(self): + print(f"[TURN] Index: {self.placing_turn_index}, Total jugadores: {len(self.client_list)}") + if self.placing_turn_index >= len(self.client_list): + # Todos han puesto bombas - iniciar fase de bรบsqueda + print("[PHASE] Cambiando a PLAYING") + self.state = STATE_PLAYING + self.playing_turn_index = 0 # Empieza el primer jugador + self._broadcast_unlocked({ + "type": "PHASE_PLAY", + "msg": "ยกA buscar! Recordad dรณnde estaban las bombas." + }) + # Notificar turno del primer jugador + self.notify_playing_turn() + return + + current_addr = self.client_list[self.placing_turn_index] + self.current_player_bombs_placed = 0 + + turn_msg = { + "type": "TURN_NOTIFY", + "active_player": str(current_addr), + "msg": f"Turno de {current_addr} para poner bombas." + } + print(f"[BROADCAST] TURN_NOTIFY: {turn_msg}") + self._broadcast_unlocked(turn_msg) + + def notify_playing_turn(self): + """Notifica el turno actual en la fase de bรบsqueda""" + if self.playing_turn_index >= len(self.client_list): + self.playing_turn_index = 0 # Volver al principio + + current_addr = self.client_list[self.playing_turn_index] + turn_msg = { + "type": "SEARCH_TURN", + "active_player": str(current_addr), + "msg": f"๐Ÿ” Turno de {current_addr} para excavar." + } + print(f"[SEARCH_TURN] Jugador {self.playing_turn_index}: {current_addr}") + self._broadcast_unlocked(turn_msg) + + def handle_client(self, client_sock, addr): + print(f"[CONECTADO] {addr}") + with self.lock: + self.clients[client_sock] = addr + if addr not in self.lives: + self.lives[addr] = 3 -def handle_client(client_socket: socket.socket, client_address): - print(f"[NUEVO CLIENTE] {client_address} conectado.") - try: - while True: - data = client_socket.recv(4096) - if not data: - break - text = data.decode('utf-8', errors='replace') - print(f"[{client_address}] {text}") - # Re-enviar a los demรกs - broadcast(data, client_socket) - except Exception as e: - print(f"[ERROR] {client_address}:", e) - finally: - with clients_lock: - if client_socket in clients: - clients.remove(client_socket) try: - client_socket.close() - except Exception: - pass - print(f"[DESCONECTADO] {client_address} cerrado.") + while True: + data = client_sock.recv(4096) + if not data: break + + try: + text_chunk = data.decode('utf-8', errors='replace') + lines = text_chunk.split('\n') + for line in lines: + line = line.strip() + if not line: continue + try: + msg = json.loads(line) + self.process_message(client_sock, addr, msg) + except json.JSONDecodeError: + pass + except Exception as e: + print(f"Error decode {addr}: {e}") + except Exception as e: + print(f"Error {addr}: {e}") + finally: + with self.lock: + self.remove_client(client_sock) + + def process_message(self, client, addr, msg): + msg_type = msg.get('type') + print(f"[MSG] {addr}: {msg_type}") # Debug log + + with self.lock: + if msg_type == 'START_GAME': + print(f"[START_GAME] Estado actual: {self.state}, Clientes: {len(self.clients)}") + if self.state == STATE_LOBBY: + # Reiniciar juego pero mantener clientes + self.round = 1 + self.bombs = set() + self.revealed = set() + + # Re-registrar vidas para los presentes + self.lives = {} + for c_addr in self.clients.values(): + self.lives[c_addr] = 3 + + print(f"[START_GAME] Iniciando ronda 1 con {len(self.clients)} jugadores") + self.start_round() + else: + print(f"[START_GAME] Ignorado - estado: {self.state}") + + elif msg_type == 'PLACE_BOMB': + if self.state == STATE_PLACING: + # Verificar turno + current_turn_addr = self.client_list[self.placing_turn_index] + if str(addr) != str(current_turn_addr): + return # No es su turno + + x, y = msg['x'], msg['y'] + # Evitar poner bomba donde ya hay (opcional, o permite solapar) + if (x,y) in self.bombs: + return # Ya hay bomba aqui + + self.bombs.add((x, y)) + self.current_player_bombs_placed += 1 + + # FLASH: Mostrar bomba 1 segundo + self._broadcast_unlocked({ + "type": "BOMB_flash", + "x": x, "y": y, + "who": str(addr) + }) + + if self.current_player_bombs_placed >= self.bombs_to_place_per_player: + self.placing_turn_index += 1 + self.next_placement_turn() + + elif msg_type == 'CLICK_CELL': + if self.state == STATE_PLAYING: + # Verificar turno + current_turn_addr = self.client_list[self.playing_turn_index] + if str(addr) != str(current_turn_addr): + print(f"[CLICK_CELL] Ignorado - no es el turno de {addr}") + return # No es su turno + + x, y = msg['x'], msg['y'] + + # Verificar si ya fue revelada + if (x, y) in self.revealed: + print(f"[CLICK_CELL] Celda {x},{y} ya revelada") + return # Ya revelada, no contar como turno + + if (x, y) in self.bombs: + # BOOM logic - el jugador pierde una vida + self.lives[addr] -= 1 + + # Notificar explosiรณn a todos + self._broadcast_unlocked({ + "type": "EXPLOSION", + "x": x, "y": y, + "who": str(addr), + "lives": self.lives[addr], + "player_addr": str(addr) + }) + + # Lร“GICA DE AVANCE DE RONDA + # Verificar si el jugador perdiรณ todas sus vidas + if self.lives[addr] <= 0: + # ยกEste jugador perdiรณ! El otro gana + self._broadcast_unlocked({ + "type": "GAME_OVER", + "loser": str(addr), + "msg": f"๐Ÿ’€ ยก{addr} ha perdido todas sus vidas! ยกGAME OVER!" + }) + self.reset_game() + return + + # Si estamos en ronda 5 o mรกs, repetir la ronda + if self.round >= 5: + self._broadcast_unlocked({ + "type": "ROUND_ADVANCE", + "msg": f"๐Ÿ’ฅ ยกExplosiรณn! Repitiendo ronda final..." + }) + # Repetir misma ronda (no incrementar) + threading.Timer(3.0, self.start_round_with_lock).start() + else: + # Avanzar a siguiente ronda + self.round += 1 + self._broadcast_unlocked({ + "type": "ROUND_ADVANCE", + "msg": f"๐Ÿ’ฅ ยกExplosiรณn! Pasando a ronda {self.round}..." + }) + threading.Timer(3.0, self.start_round_with_lock).start() + return # No avanzar turno, la ronda terminรณ + + else: + # Safe + self.revealed.add((x, y)) + self._broadcast_unlocked({ + "type": "SAFE", + "x": x, "y": y + }) + + # Avanzar turno + self.playing_turn_index += 1 + if self.playing_turn_index >= len(self.client_list): + self.playing_turn_index = 0 + self.notify_playing_turn() + + elif msg_type == 'CHECK_DUNGEON_CLEARED': + # Solo puede verificar el jugador que tiene el turno + if self.state == STATE_PLAYING: + current_turn_addr = self.client_list[self.playing_turn_index] + if str(addr) != str(current_turn_addr): + self._broadcast_unlocked({ + "type": "WARNING", + "msg": "๐Ÿ›‘ ยกSolo el jugador activo puede reclamar victoria!" + }) + return + + # Validar + bombs_exploded = 0 # No trackeamos exploded aparte, pero asumimos que siguen en self.bombs + + # Logic: Si quedan celdas NO reveladas y NO son bombas -> Faltan cosas + # Grid total + total_cells = self.grid_size * self.grid_size + safe_cells_count = total_cells - len(self.bombs) + + # Count safely revealed + # revealed set only contains SAFE cells based on logic above + if len(self.revealed) >= safe_cells_count: + # Win Round + self._broadcast_unlocked({"type": "ROUND_WIN", "msg": "ยกZona despejada!"}) + self.round += 1 + if self.round > 5: + self._broadcast_unlocked({"type": "GAME_WIN", "msg": "ยกJUEGO COMPLETADO!"}) + self.reset_game() + else: + threading.Timer(3.0, self.start_round).start() + else: + self._broadcast_unlocked({ + "type": "WARNING", + "msg": "ยกAรบn hay zonas sin explorar!" + }) def start_server(): - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind((HOST, PORT)) - server.listen(10) - print(f"[INICIO] Servidor escuchando en {HOST}:{PORT}") - - try: - while True: - client_socket, client_address = server.accept() - with clients_lock: - clients.append(client_socket) - t = threading.Thread(target=handle_client, args=(client_socket, client_address), daemon=True) - t.start() - except KeyboardInterrupt: - print('\n[APAGANDO] Servidor detenido por el usuario') - finally: - with clients_lock: - for c in clients: - try: - c.close() - except Exception: - pass - try: - server.close() - except Exception: - pass + server = GameServer() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((HOST, PORT)) + s.listen(5) + print(f"Servidor Juego escuchando en {HOST}:{PORT}") + + while True: + conn, addr = s.accept() + t = threading.Thread(target=server.handle_client, args=(conn, addr), daemon=True) + t.start() if __name__ == '__main__': start_server()