feat: Implement multiplayer Minesweeper game server with JSON protocol, game states, and turn-based logic, alongside a new game client.
This commit is contained in:
parent
a8f99f02a8
commit
afac3609cb
468
README.md
468
README.md
|
|
@ -1,73 +1,447 @@
|
|||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Python-3.8+-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python">
|
||||
<img src="https://img.shields.io/badge/Tkinter-GUI-green?style=for-the-badge&logo=python&logoColor=white" alt="Tkinter">
|
||||
<img src="https://img.shields.io/badge/TCP/IP-Multiplayer-orange?style=for-the-badge&logo=socketdotio&logoColor=white" alt="Sockets">
|
||||
<img src="https://img.shields.io/badge/License-Educational-purple?style=for-the-badge" alt="License">
|
||||
</p>
|
||||
|
||||
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.
|
||||
<h1 align="center">💣 Minesweeper Multiplayer + Dashboard</h1>
|
||||
|
||||
## 🎬 Demo en video
|
||||
<p align="center">
|
||||
<strong>Un proyecto completo de programación de servicios y procesos</strong><br>
|
||||
<em>Juego de buscaminas competitivo en red + Panel de control integral</em>
|
||||
</p>
|
||||
|
||||
- YouTube: https://youtu.be/HgJwU_HagD8
|
||||
- Drive: https://drive.google.com/file/d/14wGkkyZ9ASbV__O2xp1zIZJxSA6gpJoF/view?usp=sharing
|
||||
<p align="center">
|
||||
<a href="#-características">Características</a> •
|
||||
<a href="#-arquitectura">Arquitectura</a> •
|
||||
<a href="#-instalación">Instalación</a> •
|
||||
<a href="#-uso">Uso</a> •
|
||||
<a href="#-mecánicas-del-juego">Mecánicas</a> •
|
||||
<a href="#-tecnologías">Tecnologías</a>
|
||||
</p>
|
||||
|
||||
## Foto
|
||||
---
|
||||
|
||||
<img width="2879" height="1799" alt="image" src="https://github.com/user-attachments/assets/066099a3-316d-4443-a871-6c2a70bf2d5c" />
|
||||
## 📸 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
|
||||
<details>
|
||||
<summary>📦 <strong>Ver dependencias detalladas</strong></summary>
|
||||
|
||||
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
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 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
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="150">
|
||||
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg" width="48" height="48" alt="Python" />
|
||||
<br><strong>Python 3</strong>
|
||||
<br><em>Lenguaje base</em>
|
||||
</td>
|
||||
<td align="center" width="150">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Virtualbox_logo.png/64px-Virtualbox_logo.png" width="48" height="48" alt="Tkinter" />
|
||||
<br><strong>Tkinter</strong>
|
||||
<br><em>Interfaz gráfica</em>
|
||||
</td>
|
||||
<td align="center" width="150">
|
||||
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/socketio/socketio-original.svg" width="48" height="48" alt="Sockets" />
|
||||
<br><strong>TCP Sockets</strong>
|
||||
<br><em>Comunicación en red</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://matplotlib.org/stable/_static/logo_light.svg" width="48" height="48" alt="Matplotlib" />
|
||||
<br><strong>Matplotlib</strong>
|
||||
<br><em>Gráficos</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://raw.githubusercontent.com/devicons/devicon/master/icons/json/json-original.svg" width="48" height="48" alt="JSON" />
|
||||
<br><strong>JSON</strong>
|
||||
<br><em>Protocolo mensajes</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://www.vectorlogo.zone/logos/pocoo_flask/pocoo_flask-icon.svg" width="48" height="48" alt="Requests" />
|
||||
<br><strong>Requests</strong>
|
||||
<br><em>APIs HTTP</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🧵 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**.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<strong>Desarrollado con ❤️ por Marcos Ferrandiz</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<em>Proyecto 1º Evaluación - PSP (Programación de Servicios y Procesos)</em>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Estado-Completado-success?style=flat-square" alt="Estado">
|
||||
<img src="https://img.shields.io/badge/Versión-1.0-blue?style=flat-square" alt="Versión">
|
||||
<img src="https://img.shields.io/badge/Made%20with-Python-yellow?style=flat-square&logo=python" alt="Python">
|
||||
</p>
|
||||
|
|
|
|||
496
app.py
496
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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
418
servidor.py
418
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue