|
|
||
|---|---|---|
| __pycache__ | ||
| README.md | ||
| app.py | ||
| cliente_juego.py | ||
| requirements.txt | ||
| servidor.py | ||
README.md
💣 Minesweeper Multiplayer + Dashboard
Un proyecto completo de programación de servicios y procesos
Juego de buscaminas competitivo en red + Panel de control integral
Características • Arquitectura • Instalación • Uso • Mecánicas • Tecnologías
📸 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
🎮 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! |
📊 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
🏗️ 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
git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git
cd Proyecto1AVApsp
Paso 2: Crear Entorno Virtual (Recomendado)
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
Paso 3: Instalar Dependencias
pip install -r requirements.txt
📦 Ver dependencias detalladas
| 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) |
🎯 Uso
🖥️ Iniciar el Servidor
python servidor.py
El servidor escuchará en
0.0.0.0:3333
🎮 Opción A: Dashboard Completo
python app.py
Incluye el juego integrado + todas las funcionalidades del panel.
🎮 Opción B: Cliente Standalone
python cliente_juego.py
Cliente ligero solo para jugar al Minesweeper.
🔌 Conectar al Juego
- Introduce el Host del servidor (por defecto:
127.0.0.1) - Verifica el Puerto (por defecto:
3333) - Pulsa "Conectar"
- ¡Espera a otro jugador y pulsa "Iniciar Juego"!
💣 Mecánicas del Juego
🔄 Flujo de una Partida
┌─────────────────────────────────────────────────────────────────┐
│ 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) │
│ │
└─────────────────────────────────────────────────────────────────┘
📊 Progresión de Dificultad
| 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 |
*Para 2 jugadores
🎯 Fases del Juego
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!
2. 🔍 Fase de Búsqueda (PLAYING)
- Excava casillas por turnos
- Casilla segura → Se marca en verde ✅
- Bomba → ¡EXPLOSIÓN! Pierdes 1 vida 💔
🏆 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
// 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
// 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!"
}
🔧 Documentación Técnica Detallada
🆔 Sistema de Identificación de Jugadores
El servidor identifica a cada jugador mediante su dirección de socket, que es una tupla única (IP, Puerto):
┌─────────────────────────────────────────────────────────────────┐
│ IDENTIFICACIÓN ÚNICA DE JUGADORES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Cuando un cliente se conecta al servidor: │
│ │
│ Cliente 1 ──────► sock.accept() ──────► ('127.0.0.1', 49956) │
│ Cliente 2 ──────► sock.accept() ──────► ('127.0.0.1', 49968) │
│ │
│ Aunque ambos clientes estén en el MISMO ordenador (127.0.0.1) │
│ el PUERTO es diferente y único para cada conexión. │
│ │
│ Esta tupla (IP, Puerto) actúa como IDENTIFICADOR ÚNICO. │
│ │
└─────────────────────────────────────────────────────────────────┘
¿Cómo funciona en el mismo ordenador?
# servidor.py - Al aceptar conexión
conn, addr = s.accept() # addr = ('127.0.0.1', 49956)
# El sistema operativo asigna un puerto efímero ÚNICO
# a cada nueva conexión de socket del cliente
| Cliente | IP | Puerto (asignado por SO) | Identificador Completo |
|---|---|---|---|
| Dashboard 1 | 127.0.0.1 | 49956 | ('127.0.0.1', 49956) |
| Dashboard 2 | 127.0.0.1 | 49968 | ('127.0.0.1', 49968) |
| Cliente remoto | 192.168.1.50 | 52341 | ('192.168.1.50', 52341) |
💡 Clave: El puerto del cliente es asignado automáticamente por el sistema operativo y es siempre diferente para cada nueva conexión, incluso desde el mismo ordenador.
🔄 Gestión de Turnos
El servidor mantiene dos índices de turno para controlar quién juega:
# servidor.py - Variables de control de turnos
self.placing_turn_index = 0 # Índice en fase PLACING
self.playing_turn_index = 0 # Índice en fase PLAYING
self.client_list = [] # Lista ordenada de direcciones
Flujo de Verificación de Turno
┌─────────────────────────────────────────────────────────────────┐
│ VERIFICACIÓN DE TURNO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ client_list = [('127.0.0.1', 49956), ('127.0.0.1', 49968)] │
│ ↑ ↑ │
│ índice 0 índice 1 │
│ │
│ Si placing_turn_index = 0: │
│ → Solo ('127.0.0.1', 49956) puede poner bombas │
│ │
│ Cuando recibe PLACE_BOMB de un cliente: │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ if str(addr) != str(current_turn_addr): │ │
│ │ return # No es su turno, ignorar │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Código de Verificación (servidor.py)
elif msg_type == 'PLACE_BOMB':
if self.state == STATE_PLACING:
# Obtener quién tiene el turno actual
current_turn_addr = self.client_list[self.placing_turn_index]
# Comparar con quién envió el mensaje
if str(addr) != str(current_turn_addr):
return # ¡No es tu turno! Ignorar mensaje
# Procesar la bomba...
👤 Cómo el Cliente Sabe si es su Turno
El cliente guarda su propia dirección al conectarse y la compara con los mensajes del servidor:
# app.py - Al conectarse
self.my_address = str(sock.getsockname()) # Ej: "('127.0.0.1', 49956)"
# Al recibir TURN_NOTIFY del servidor
def handle_message(self, msg):
if mtype == 'TURN_NOTIFY':
active_player = msg.get('active_player') # "('127.0.0.1', 49956)"
# Comparar con mi dirección
if active_player == self.my_address:
self._log_game("🎯 ¡ES TU TURNO!")
# Habilitar controles...
else:
self._log_game(f"⏳ Turno de {active_player}")
# Deshabilitar controles...
┌─────────────────────────────────────────────────────────────────┐
│ FLUJO DE NOTIFICACIÓN DE TURNO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SERVIDOR CLIENTES │
│ ──────── ──────── │
│ │
│ broadcast({ Cliente 1 (49956): │
│ "type": "TURN_NOTIFY", ├─ my_address = 49956 │
│ "active_player": ├─ active_player = 49956 │
│ "('127.0.0.1', 49956)" └─ ✓ ¡ES MI TURNO! │
│ }) │
│ │ Cliente 2 (49968): │
│ └──────────────────────────► ├─ my_address = 49968 │
│ ├─ active_player = 49956 │
│ └─ ✗ No es mi turno │
│ │
└─────────────────────────────────────────────────────────────────┘
🔒 Sincronización con Threading Lock
El servidor usa un threading.Lock para evitar condiciones de carrera cuando múltiples clientes envían mensajes simultáneamente:
class GameServer:
def __init__(self):
self.lock = threading.Lock() # Mutex para sincronización
self.clients = {} # Diccionario compartido
self.state = STATE_LOBBY # Estado compartido
def process_message(self, client, addr, msg):
with self.lock: # Adquirir lock antes de modificar estado
if msg_type == 'PLACE_BOMB':
# Solo un hilo puede ejecutar esto a la vez
self.bombs.add((x, y))
self._broadcast_unlocked(...)
┌─────────────────────────────────────────────────────────────────┐
│ SINCRONIZACIÓN CON LOCK │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Hilo Cliente 1 ─────┐ │
│ ├──► with self.lock: ──► Ejecuta primero │
│ Hilo Cliente 2 ─────┘ │ │
│ ▼ │
│ (espera...) │
│ │ │
│ ▼ │
│ Hilo 2 ejecuta después │
│ │
│ Esto evita que dos jugadores modifiquen el estado │
│ del juego al mismo tiempo (condiciones de carrera). │
│ │
└─────────────────────────────────────────────────────────────────┘
📡 Arquitectura de Hilos
┌─────────────────────────────────────────────────────────────────┐
│ ARQUITECTURA DE HILOS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SERVIDOR (servidor.py) │
│ ══════════════════════ │
│ │
│ ┌─────────────────┐ │
│ │ Hilo Main │ ◄── Acepta conexiones (s.accept()) │
│ └────────┬────────┘ │
│ │ │
│ ├──► Hilo Cliente 1 ──► handle_client(sock1) │
│ ├──► Hilo Cliente 2 ──► handle_client(sock2) │
│ └──► Hilo Cliente N ──► handle_client(sockN) │
│ │
│ Cada cliente tiene su propio hilo daemon que: │
│ 1. Lee mensajes del socket (recv) │
│ 2. Procesa el mensaje (process_message) │
│ 3. Puede hacer broadcast a todos los clientes │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ CLIENTE (app.py) │
│ ════════════════ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Hilo Main │ │ Hilo Recv │ │
│ │ (Tkinter UI) │◄───│ (_recv_loop) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ ▲ │
│ │ msg_queue.put() │ sock.recv() │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ Cola de msgs │ ──────────┘ │
│ │ (thread-safe) │ │
│ └─────────────────┘ │
│ │
│ El hilo de recepción pone mensajes en una cola. │
│ El hilo principal (UI) los procesa con after(). │
│ │
└─────────────────────────────────────────────────────────────────┘
🎮 Resumen: Jugar en el Mismo Ordenador
| Paso | Qué Sucede |
|---|---|
| 1. Iniciar servidor | python servidor.py escucha en puerto 3333 |
| 2. Abrir Cliente 1 | Conecta → SO asigna puerto 49956 |
| 3. Abrir Cliente 2 | Conecta → SO asigna puerto 49968 |
| 4. Servidor registra | clients = {sock1: ('127.0.0.1', 49956), sock2: ('127.0.0.1', 49968)} |
| 5. Iniciar juego | client_list = [addr1, addr2] define orden de turnos |
| 6. Turno jugador 1 | Servidor envía TURN_NOTIFY con active_player = addr1 |
| 7. Cliente 1 compara | my_address == active_player → ¡Es mi turno! |
| 8. Cliente 2 compara | my_address != active_player → Esperar |
| 9. Jugador 1 actúa | Envía PLACE_BOMB o CLICK_CELL |
| 10. Servidor valida | addr == current_turn_addr → Válido, procesar |
| 11. Avanzar turno | placing_turn_index += 1 → Turno del siguiente |
🛠️ Tecnologías
|
Python 3 Lenguaje base |
Tkinter Interfaz gráfica |
TCP Sockets Comunicación en red |
|
Matplotlib Gráficos |
JSON Protocolo mensajes |
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
- Fork del proyecto
- Crea tu Feature Branch (
git checkout -b feature/NuevaFuncion) - Commit tus cambios (
git commit -m 'Add: Nueva función') - Push a la rama (
git push origin feature/NuevaFuncion) - 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)