Proyecto1AVApsp/README.md

34 KiB
Raw Blame History

Python Tkinter Sockets License

💣 Minesweeper Multiplayer + Dashboard

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

CaracterísticasArquitecturaInstalaciónUsoMecánicasTecnologí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

  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

┌─────────────────────────────────────────────────────────────────┐
│                    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
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