Proyecto 1 AVA psp
Go to file
marcos 0c235f0c9f docs: Add in-depth technical analysis of bomb duplicate detection system
- Add mathematical foundations (set theory) explanation
- Document internal workings of Python set() with hash tables
- Include algorithmic complexity analysis (O(1) vs O(n))
- Explain step-by-step validation process with memory state
- Add race condition scenarios and threading.Lock solution
- Provide mathematical proofs (idempotence, state consistency)
- Compare client vs server validation architectures
- Include memory and performance optimization analysis
- Add 800+ lines of detailed technical documentation
2026-02-19 20:19:04 +01:00
.gitignore correo 2026-02-19 20:02:35 +01:00
ARQUITECTURA_CORREO.md correo 2026-02-19 20:02:35 +01:00
CORREO_README.md correo 2026-02-19 20:02:35 +01:00
README.md docs: Add in-depth technical analysis of bomb duplicate detection system 2026-02-19 20:19:04 +01:00
TESTING_GUIDE.md correo 2026-02-19 20:02:35 +01:00
app.py correo 2026-02-19 20:02:35 +01:00
cliente_juego.py feat: Implement multiplayer Minesweeper game server with JSON protocol, game states, and turn-based logic, alongside a new game client. 2026-02-02 17:22:11 +01:00
requirements.txt correo 2026-02-19 20:02:35 +01:00
servidor.py feat: Implement multiplayer Minesweeper game server with JSON protocol, game states, and turn-based logic, alongside a new game client. 2026-02-02 17:22:11 +01:00
test_mail_server.py correo 2026-02-19 20:02:35 +01:00
test_startup.py correo 2026-02-19 20:02:35 +01:00

README.md

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í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

📧 Cliente de Correo ElectrÃģnico Completo

Característica DescripciÃģn
📎 IMAP Lectura de correos desde servidor Webmin (Puerto 143)
ðŸ“Ī SMTP Envío de correos con autenticaciÃģn (Puerto 25)
ðŸ’ū Auto-guardado Credenciales guardadas con Base64
ðŸ‘Ĩ MÚltiples destinatarios Envío a varios correos simultÃĄneamente
📎 Adjuntos Soporte para imÃĄgenes, PDFs, docs, Excel, ZIP
🖞ïļ ImÃĄgenes inline VisualizaciÃģn de imÃĄgenes dentro del correo
🗂ïļ Carpetas Bandeja de entrada y enviados sincronizados
ðŸ”ĩ Indicadores Correos leídos/no leídos con marcadores visuales

🏗ïļ 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:62-65 - 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:222-232 - VerificaciÃģn de turno al colocar bomba
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:1221-1240 - IdentificaciÃģn del cliente y manejo de turno
# 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         │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

ðŸ’Ģ DetecciÃģn de Bombas Duplicadas

El servidor usa un Set de Python para almacenar las coordenadas de las bombas, lo que garantiza que no haya duplicados.

Estructura de Datos: self.bombs = set()

# servidor.py:32 - InicializaciÃģn del set de bombas
class GameServer:
    def __init__(self):
        self.bombs = set()  # Set de tuplas (x, y)

Un set() en Python no permite elementos duplicados. Si intentas agregar la misma tupla (x, y) dos veces, solo se guarda una vez.

ValidaciÃģn al Colocar Bomba

# servidor.py:222-246 - ValidaciÃģn completa de colocaciÃģn de bomba
elif msg_type == 'PLACE_BOMB':
    if self.state == STATE_PLACING:
        # 1. Verificar que es el turno del jugador
        current_turn_addr = self.client_list[self.placing_turn_index]
        if str(addr) != str(current_turn_addr):
            return  # ❌ No es su turno, ignorar
        
        x, y = msg['x'], msg['y']
        
        # 2. ⭐ VERIFICAR SI YA HAY BOMBA EN ESA CASILLA
        if (x, y) in self.bombs:
            return  # ❌ Ya hay bomba aquí, ignorar clic
        
        # 3. ✅ Agregar bomba al set
        self.bombs.add((x, y))
        self.current_player_bombs_placed += 1
        
        # 4. Mostrar flash de bomba a todos (1 segundo)
        self._broadcast_unlocked({
            "type": "BOMB_flash",
            "x": x, "y": y,
            "who": str(addr)
        })
        
        # 5. Si el jugador completÃģ sus bombas, pasar al siguiente
        if self.current_player_bombs_placed >= self.bombs_to_place_per_player:
            self.placing_turn_index += 1
            self.next_placement_turn()

Flujo Completo de ValidaciÃģn

┌─────────────────────────────────────────────────────────────────────┐
│             VALIDACIÓN DE COLOCACIÓN DE BOMBA                        │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  Jugador 1 hace clic en casilla (2, 1)                              │
│     │                                                                │
│     ├──▹ Mensaje enviado: {"type": "PLACE_BOMB", "x": 2, "y": 1}   │
│     │                                                                │
│     └──▹ SERVIDOR recibe mensaje                                    │
│             │                                                        │
│             ├─▹ VALIDACIÓN 1: ÂŋEs el turno de este jugador?         │
│             │   ├─ current_turn_addr = client_list[placing_index]   │
│             │   └─ if addr != current_turn_addr: return ❌          │
│             │                                                        │
│             ├─▹ VALIDACIÓN 2: ÂŋYa hay bomba en (2, 1)?              │
│             │   ├─ if (2, 1) in self.bombs:                         │
│             │   │     return  # ❌ Ignorar clic                      │
│             │   └─ self.bombs = {(0,0), (1,1)} → NO contiene (2,1)  │
│             │                           ✅ VÁLIDO                    │
│             │                                                        │
│             ├─▹ AGREGAR BOMBA:                                       │
│             │   └─ self.bombs.add((2, 1))                            │
│             │      → self.bombs = {(0,0), (1,1), (2,1)}             │
│             │                                                        │
│             ├─▹ BROADCAST a todos:                                   │
│             │   └─ {"type": "BOMB_flash", "x": 2, "y": 1}           │
│             │                                                        │
│             └─▹ INCREMENTAR contador:                                │
│                 └─ current_player_bombs_placed += 1                 │
│                                                                      │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                      │
│  Jugador 1 hace clic OTRA VEZ en (2, 1) por error                   │
│     │                                                                │
│     └──▹ SERVIDOR recibe: {"type": "PLACE_BOMB", "x": 2, "y": 1}   │
│             │                                                        │
│             ├─▹ VALIDACIÓN 1: ✅ Es su turno                        │
│             │                                                        │
│             ├─▹ VALIDACIÓN 2: ÂŋYa hay bomba en (2, 1)?              │
│             │   └─ if (2, 1) in self.bombs:                         │
│             │         return  # ❌ SÍ EXISTE, IGNORAR               │
│             │                                                        │
│             └─▹ ðŸšŦ NO se procesa el clic                            │
│                 ðŸšŦ NO se envía BOMB_flash                            │
│                 ðŸšŦ NO se incrementa contador                         │
│                                                                      │
│  Resultado: El jugador puede hacer clic mÚltiples veces en la       │
│  misma casilla, pero solo la primera vez cuenta.                    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ÂŋPor quÃĐ usar un Set?

Estructura Ventaja OperaciÃģn in
set() ✅ No permite duplicados automÃĄticamente O(1) - InstantÃĄneo
list() ❌ Permite duplicados, necesita validaciÃģn manual O(n) - Lento
dict() ✅ Claves Únicas, pero mÃĄs memoria O(1) - InstantÃĄneo
# Ejemplo de eficiencia

# ❌ CON LISTA (Lento)
bombs_list = [(0,0), (1,1), (2,2)]
if (2, 1) in bombs_list:  # Revisa TODA la lista: O(n)
    return

# ✅ CON SET (RÃĄpido)
bombs_set = {(0,0), (1,1), (2,2)}
if (2, 1) in bombs_set:  # Hash lookup instantÃĄneo: O(1)
    return

Ejemplo PrÃĄctico: Ronda 1 con 2 Jugadores

Ronda 1: Grid 3×3, cada jugador coloca 3 bombas

Estado inicial:
self.bombs = set()  # Vacío
self.bombs_to_place_per_player = 3

┌───────────────────────────────────────┐
│  TURNO JUGADOR 1                      │
├───────────────────────────────────────â”Ī
│  Clic en (0, 0):                      │
│    ├─ (0, 0) in bombs? → NO           │
│    └─ bombs.add((0, 0))               │
│       → bombs = {(0,0)}               │
│                                       │
│  Clic en (1, 1):                      │
│    ├─ (1, 1) in bombs? → NO           │
│    └─ bombs.add((1, 1))               │
│       → bombs = {(0,0), (1,1)}        │
│                                       │
│  Clic en (0, 0) por error:            │
│    ├─ (0, 0) in bombs? → ✅ SÍ        │
│    └─ return (IGNORADO) ❌            │
│       → bombs = {(0,0), (1,1)}        │
│       → contador NO aumenta           │
│                                       │
│  Clic en (2, 2):                      │
│    ├─ (2, 2) in bombs? → NO           │
│    └─ bombs.add((2, 2))               │
│       → bombs = {(0,0), (1,1), (2,2)} │
│       → contador = 3 ✅               │
│       → TURNO COMPLETADO              │
└───────────────────────────────────────┘

┌───────────────────────────────────────┐
│  TURNO JUGADOR 2                      │
├───────────────────────────────────────â”Ī
│  Clic en (0, 1):                      │
│    ├─ (0, 1) in bombs? → NO           │
│    └─ bombs.add((0, 1))               │
│       → bombs = {(0,0),(1,1),(2,2),   │
│                  (0,1)}               │
│                                       │
│  Clic en (1, 1) (donde J1 puso):      │
│    ├─ (1, 1) in bombs? → ✅ SÍ        │
│    └─ return (IGNORADO) ❌            │
│       → NO puede poner bomba encima   │
│                                       │
│  Clic en (1, 2):                      │
│    └─ bombs.add((1, 2))               │
│       → bombs = {..., (1,2)}          │
│                                       │
│  Clic en (2, 0):                      │
│    └─ bombs.add((2, 0))               │
│       → bombs = {(0,0),(1,1),(2,2),   │
│                  (0,1),(1,2),(2,0)}   │
│       → 6 bombas total (3 por jugador)│
└───────────────────────────────────────┘

Grid final:
  0   1   2
┌───┮───┮───┐
│ ðŸ’Ģ│ ðŸ’Ģ│   │  0
├───┾───┾───â”Ī
│   │ ðŸ’Ģ│ ðŸ’Ģ│  1
├───┾───┾───â”Ī
│ ðŸ’Ģ│   │ ðŸ’Ģ│  2
└───â”ī───â”ī───┘

Total: 6 bombas Únicas, sin duplicados

CÃģdigo del Cliente

# app.py:1310-1330 - Manejo de clic en casilla durante fase de colocaciÃģn
def on_button_click(x, y):
    """Cuando el jugador hace clic en una casilla"""
    if self.game_phase == 'PLACING':
        # Solo permite clic si es tu turno
        if self.my_turn:
            # Enviar al servidor
            self.game_client.send({"type": "PLACE_BOMB", "x": x, "y": y})
            
            # ⚠ïļ IMPORTANTE: El cliente NO valida duplicados localmente
            # El servidor se encarga de toda la validaciÃģn
            # Si el servidor ignora el mensaje (duplicado), 
            # simplemente no recibirÃĄs BOMB_flash

Ventaja de validar en servidor: Un jugador malicioso no puede modificar el cliente para hacer trampas. El servidor tiene la Única fuente de verdad.


🔎 ANÁLISIS EN PROFUNDIDAD: SISTEMA DE DETECCIÓN DE BOMBAS DUPLICADAS

📐 Fundamentos MatemÃĄticos y Computacionales

1. Teoría de Conjuntos Aplicada

El sistema de detecciÃģn de bombas se basa en la teoría de conjuntos matemÃĄticos, donde un conjunto es una colecciÃģn de elementos Únicos sin orden específico.

DEFINICIÓN MATEMÁTICA:
━━━━━━━━━━━━━━━━━━━━

Sea B el conjunto de bombas en el grid:
B = {(x₁, y₁), (x₂, y₂), ..., (xₙ, yₙ)}

Propiedad fundamental de conjuntos:
∀ elemento e, e ∈ B → e aparece exactamente 1 vez

Intentar agregar (x, y) cuando (x, y) ∈ B:
B ∊ {(x, y)} = B  (no cambia el conjunto)

AplicaciÃģn en Python:

# servidor.py:32-35
class GameServer:
    def __init__(self):
        self.bombs = set()  # ImplementaciÃģn de conjunto matemÃĄtico

El set() de Python implementa internamente una tabla hash que garantiza unicidad en tiempo O(1).


2. Funcionamiento Interno de set() en Python

Estructura interna:

┌─────────────────────────────────────────────────────────────────┐
│                    TABLA HASH INTERNA DE SET                     │
├─────────────────────────────────────────────────────────────────â”Ī
│                                                                  │
│  Cuando haces: self.bombs.add((2, 1))                           │
│                                                                  │
│  1. CÁLCULO DEL HASH                                            │
│     ├─▹ hash((2, 1)) = hash_function(2, 1)                     │
│     └─▹ Resultado: 3713081631934410656 (entero Único)          │
│                                                                  │
│  2. ÍNDICE EN TABLA                                             │
│     ├─▹ índice = hash_value % tamaÃąo_tabla                     │
│     └─▹ índice = 3713081631934410656 % 8 = 0                   │
│                                                                  │
│  3. ALMACENAMIENTO                                              │
│     Tabla interna (simplificada):                               │
│     ┌────┮──────────────────────────────┐                       │
│     │ 0  │ → (2, 1)                     │  ← Nuestra tupla      │
│     ├────┾──────────────────────────────â”Ī                       │
│     │ 1  │ → (0, 0)                     │                       │
│     ├────┾──────────────────────────────â”Ī                       │
│     │ 2  │ → None                       │                       │
│     ├────┾──────────────────────────────â”Ī                       │
│     │ 3  │ → (1, 1)                     │                       │
│     ├────┾──────────────────────────────â”Ī                       │
│     │ 4  │ → None                       │                       │
│     ├────┾──────────────────────────────â”Ī                       │
│     │...│ ...                           │                       │
│     └────â”ī──────────────────────────────┘                       │
│                                                                  │
│  4. VERIFICACIÓN DE DUPLICADO                                   │
│     Cuando verificas: if (2, 1) in self.bombs:                 │
│     ├─▹ Calcula hash((2, 1)) nuevamente                        │
│     ├─▹ Busca en índice 0                                      │
│     ├─▹ Compara: (2, 1) == (2, 1) → True                       │
│     └─▹ Retorna: True (ya existe)                              │
│                                                                  │
│  TIEMPO DE EJECUCIÓN: O(1) - Constante                         │
│  No importa si hay 10 o 10,000 bombas, siempre es instantÃĄneo  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

CÃģdigo de demostraciÃģn:

# Ejemplo de hashing en Python
>>> tupla = (2, 1)
>>> hash(tupla)
3713081631934410656

>>> tupla2 = (2, 1)  # Mismos valores
>>> hash(tupla2)
3713081631934410656  # Mismo hash!

>>> tupla3 = (1, 2)  # Valores diferentes
>>> hash(tupla3)
3713081631934410657  # Hash diferente!

3. AnÃĄlisis de Complejidad Algorítmica

Operaciones críticas:

OperaciÃģn CÃģdigo Complejidad Temporal Complejidad Espacial
InicializaciÃģn self.bombs = set() O(1) O(1) inicial
Agregar bomba self.bombs.add((x, y)) O(1) promedio O(n) total
Verificar duplicado (x, y) in self.bombs O(1) promedio O(1)
TamaÃąo del conjunto len(self.bombs) O(1) O(1)

ComparaciÃģn con alternativas:

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ALTERNATIVA 1: LISTA (❌ INEFICIENTE)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

self.bombs = []  # Lista vacía

# Agregar bomba
x, y = 2, 1
if (x, y) not in self.bombs:  # O(n) - Recorre TODA la lista
    self.bombs.append((x, y))   # O(1)

# PROBLEMA: Con 100 bombas, verifica 100 elementos cada vez
# Tiempo total: O(n) por cada verificaciÃģn

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ALTERNATIVA 2: DICCIONARIO (✅ FUNCIONA PERO EXCESIVO)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

self.bombs = {}  # Diccionario vacío

# Agregar bomba
x, y = 2, 1
if (x, y) not in self.bombs:  # O(1) - Hash lookup
    self.bombs[(x, y)] = True  # O(1)

# PROBLEMA: Desperdicia memoria almacenando valor inÚtil (True)
# Memoria: Clave (x,y) + Valor True + Overhead

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# SOLUCIÓN ÓPTIMA: SET (✅ PERFECTO)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

self.bombs = set()  # Conjunto vacío

# Agregar bomba
x, y = 2, 1
if (x, y) in self.bombs:  # O(1) - Hash lookup
    return
self.bombs.add((x, y))    # O(1)

# VENTAJAS:
# ✅ VerificaciÃģn O(1)
# ✅ Memoria mínima (solo claves)
# ✅ SemÃĄntica clara (conjunto de coordenadas)

4. Anatomía del Proceso de ValidaciÃģn (Paso a Paso)

Vamos a analizar exactamente quÃĐ sucede en memoria cuando un jugador intenta colocar una bomba:

# servidor.py:222-246 - CÓDIGO COMPLETO CON ANOTACIONES

elif msg_type == 'PLACE_BOMB':
    if self.state == STATE_PLACING:
        # ═══════════════════════════════════════════════════════════
        # VALIDACIÓN 1: VERIFICAR TURNO
        # ═══════════════════════════════════════════════════════════
        
        # Estado actual del servidor:
        # self.placing_turn_index = 0
        # self.client_list = [('127.0.0.1', 49956), ('127.0.0.1', 49968)]
        
        current_turn_addr = self.client_list[self.placing_turn_index]
        # → current_turn_addr = ('127.0.0.1', 49956)
        
        # Mensaje recibido desde:
        # addr = ('127.0.0.1', 49968)  ← Jugador 2
        
        if str(addr) != str(current_turn_addr):
            # str(('127.0.0.1', 49968)) != str(('127.0.0.1', 49956))
            # "('127.0.0.1', 49968)" != "('127.0.0.1', 49956)"
            # True → NO es su turno
            return  # ❌ RECHAZAR mensaje, no procesar nada
        
        # Si llegamos aquí: ✅ ES EL TURNO CORRECTO
        
        # ═══════════════════════════════════════════════════════════
        # VALIDACIÓN 2: VERIFICAR DUPLICADO
        # ═══════════════════════════════════════════════════════════
        
        x, y = msg['x'], msg['y']
        # Supongamos: x = 2, y = 1
        
        # Estado actual de bombas:
        # self.bombs = {(0, 0), (1, 1), (2, 2)}
        
        # PROCESO INTERNO:
        # 1. Python calcula: hash((2, 1))
        # 2. Busca en tabla hash interna del set
        # 3. Compara valor en esa posiciÃģn
        
        if (x, y) in self.bombs:
            # BÚsqueda O(1):
            # hash((2, 1)) → buscar en tabla → No encontrado
            # Resultado: False
            # No entra al if, continÚa...
            pass
        
        # Si (2, 1) YA existiera:
        #   hash((2, 1)) → buscar en tabla → Encontrado!
        #   Resultado: True
        #   Ejecuta: return  ❌ RECHAZAR
        
        # ═══════════════════════════════════════════════════════════
        # PASO 3: AGREGAR BOMBA (SOLO SI PASÓ VALIDACIONES)
        # ═══════════════════════════════════════════════════════════
        
        self.bombs.add((x, y))
        # INTERNAMENTE:
        # 1. Calcula hash((2, 1))
        # 2. Encuentra slot vacío en tabla
        # 3. Almacena tupla (2, 1)
        # 4. Incrementa contador interno: len(self.bombs) = 4
        
        # Estado DESPUÉS:
        # self.bombs = {(0, 0), (1, 1), (2, 2), (2, 1)}
        
        self.current_player_bombs_placed += 1
        # Contador del jugador actual: 1 → 2
        
        # ═══════════════════════════════════════════════════════════
        # PASO 4: NOTIFICAR A TODOS LOS CLIENTES
        # ═══════════════════════════════════════════════════════════
        
        self._broadcast_unlocked({
            "type": "BOMB_flash",
            "x": x,
            "y": y,
            "who": str(addr)
        })
        # Envía mensaje JSON a TODOS los clientes conectados
        # Cada cliente mostrarÃĄ flash amarillo durante 1 segundo
        
        # ═══════════════════════════════════════════════════════════
        # PASO 5: VERIFICAR SI COMPLETÓ SUS BOMBAS
        # ═══════════════════════════════════════════════════════════
        
        if self.current_player_bombs_placed >= self.bombs_to_place_per_player:
            # Si colocÃģ todas sus bombas (ej: 3/3)
            self.placing_turn_index += 1  # Pasar al siguiente jugador
            self.next_placement_turn()     # Notificar nuevo turno

5. Escenarios de Error y Manejo

Escenario A: Doble Clic Accidental

SITUACIÓN: Usuario hace doble clic rÃĄpido en la misma casilla

TIMELINE:
─────────

t=0ms:   Clic 1 en (2,1)
         ├─▹ Cliente envía: {"type": "PLACE_BOMB", "x": 2, "y": 1}
         └─▹ Red: ~5ms latencia

t=5ms:   Servidor recibe mensaje 1
         ├─▹ ValidaciÃģn turno: ✅ OK
         ├─▹ ValidaciÃģn duplicado: (2,1) in bombs → False ✅
         ├─▹ self.bombs.add((2,1)) → bombs = {..., (2,1)}
         ├─▹ Broadcast BOMB_flash
         └─▹ current_player_bombs_placed = 1

t=50ms:  Clic 2 en (2,1) (doble clic accidental)
         ├─▹ Cliente envía: {"type": "PLACE_BOMB", "x": 2, "y": 1}
         └─▹ Red: ~5ms latencia

t=55ms:  Servidor recibe mensaje 2
         ├─▹ ValidaciÃģn turno: ✅ OK (sigue siendo su turno)
         ├─▹ ValidaciÃģn duplicado: (2,1) in bombs → True ❌
         └─▹ return  (IGNORA el mensaje completamente)

RESULTADO:
  â€Ē Solo la primera bomba se cuenta
  â€Ē No hay feedback visual al usuario (silenciosamente ignorado)
  â€Ē Contador permanece en 1 (no se incrementa)
  â€Ē Usuario puede hacer clic en otra casilla

Escenario B: Jugador Intenta Poner Bomba Donde Ya Puso Oponente

ESTADO ACTUAL:
  Jugador 1 ya colocÃģ bomba en (1, 1)
  self.bombs = {(0,0), (1,1), (2,2)}
  Ahora es turno de Jugador 2

INTENTO:
  Jugador 2 hace clic en (1, 1)

VALIDACIÓN:
  ├─▹ Turno: ✅ Es Jugador 2, correcto
  ├─▹ Duplicado: (1,1) in bombs → True ❌
  └─▹ return (RECHAZADO)

EFECTO:
  â€Ē Jugador 2 NO puede poner bomba ahí
  â€Ē No recibe ningÚn feedback visual
  â€Ē Debe elegir otra casilla
  â€Ē El sistema protege la integridad del grid

Escenario C: CondiciÃģn de Carrera (Race Condition)

PROBLEMA POTENCIAL (sin threading.Lock):
  Dos clientes envían mensaje al MISMO TIEMPO

Cliente A (simultÃĄneo)              Cliente B (simultÃĄneo)
     │                                      │
     ├─▹ PLACE_BOMB (2,1)                  ├─▹ PLACE_BOMB (2,1)
     │                                      │
     ▾                                      ▾
┌─────────────────────────────────────────────────────┐
│              SERVIDOR (sin Lock)                     │
├─────────────────────────────────────────────────────â”Ī
│                                                      │
│  Hilo A:                    Hilo B:                 │
│  ├─ (2,1) in bombs → False  ├─ (2,1) in bombs → False
│  ├─ bombs.add((2,1))        ├─ bombs.add((2,1))    │
│  └─ contador += 1           └─ contador += 1       │
│                                                      │
│  RESULTADO: ¡AMBOS se agregan! (BUG)               │
│  contador = 2 (cuando debería ser 1)                │
└─────────────────────────────────────────────────────┘

SOLUCIÓN CON threading.Lock:

Cliente A                           Cliente B
     │                                  │
     ├─▹ PLACE_BOMB (2,1)              ├─▹ PLACE_BOMB (2,1)
     │                                  │
     ▾                                  ▾
┌─────────────────────────────────────────────────────┐
│           SERVIDOR (con Lock)                        │
├─────────────────────────────────────────────────────â”Ī
│                                                      │
│  Hilo A:                                            │
│  ├─▹ with self.lock:  ← ADQUIERE LOCK              │
│  │     ├─ (2,1) in bombs → False                   │
│  │     ├─ bombs.add((2,1))                         │
│  │     └─ contador += 1                            │
│  └─▹ LIBERA LOCK                                    │
│                                                      │
│  Hilo B:                                            │
│  ├─▹ with self.lock:  ← ESPERA... ESPERA...        │
│  │   (bloqueado hasta que A termine)               │
│  └─▹ ADQUIERE LOCK cuando A termina                │
│        ├─ (2,1) in bombs → True ✅ (A ya la puso)  │
│        └─ return (RECHAZADO correctamente)         │
│                                                      │
│  RESULTADO: Solo A se agrega ✅                     │
└─────────────────────────────────────────────────────┘

ImplementaciÃģn del Lock:

# servidor.py:115-125
def process_message(self, client, addr, msg):
    with self.lock:  # ← PUNTO CRÍTICO: ExclusiÃģn mutua
        msg_type = msg.get('type')
        
        if msg_type == 'PLACE_BOMB':
            # Todo el cÃģdigo de validaciÃģn aquí
            # Solo UN hilo puede ejecutar esto a la vez
            pass

6. Prueba de Propiedades MatemÃĄticas

Propiedad 1: Idempotencia

DEFINICIÓN: Aplicar la misma operaciÃģn mÚltiples veces 
            produce el mismo resultado que aplicarla una vez

PRUEBA:
  Sea B = {(0,0), (1,1)}
  
  OperaciÃģn: Agregar (2,1)
  
  B.add((2,1)) → B = {(0,0), (1,1), (2,1)}
  B.add((2,1)) → B = {(0,0), (1,1), (2,1)}  ← Mismo resultado
  B.add((2,1)) → B = {(0,0), (1,1), (2,1)}  ← Mismo resultado
  
  âˆī La operaciÃģn add en set es IDEMPOTENTE ✅

Propiedad 2: Consistencia de Estado

INVARIANTE: El nÚmero de bombas en self.bombs debe ser igual
            a la suma de bombas colocadas por todos los jugadores

PRUEBA POR INDUCCIÓN:

Base (n=0): 
  Inicio del juego
  self.bombs = set() → len(bombs) = 0
  Jugadores han colocado 0 bombas
  0 = 0 ✅

Paso inductivo:
  Supongamos cierto para k bombas: len(bombs) = k
  
  Al colocar bomba k+1:
    Si (x,y) ∉ bombs:
      → bombs.add((x,y))
      → len(bombs) = k + 1 ✅
    
    Si (x,y) ∈ bombs:
      → return (no se agrega)
      → len(bombs) = k ✅ (mantiene invariante)
  
  âˆī La invariante se mantiene siempre ✅

7. Ventajas de Arquitectura Cliente-Servidor

ValidaciÃģn en Servidor vs Cliente:

┌─────────────────────────────────────────────────────────────┐
│              COMPARACIÓN DE ARQUITECTURAS                    │
├─────────────────────────────────────────────────────────────â”Ī
│                                                              │
│  ❌ VALIDACIÓN EN CLIENTE (INSEGURA)                        │
│  ════════════════════════════════════════                   │
│                                                              │
│  Cliente A                    Cliente B                      │
│  ├─ Valida localmente        ├─ Valida localmente          │
│  ├─ Envía si vÃĄlido          ├─ Envía si vÃĄlido            │
│  └─ PROBLEMA:                └─ PROBLEMA:                  │
│     â€Ē Jugador malicioso         â€Ē Clientes pueden          │
│       modifica cÃģdigo            desincronizarse           │
│     â€Ē Envía bombas              â€Ē Grid diferente en        │
│       duplicadas                 cada cliente              │
│     â€Ē Hace trampa               â€Ē Inconsistencia           │
│                                                              │
│  ✅ VALIDACIÓN EN SERVIDOR (SEGURA)                         │
│  ══════════════════════════════════════                     │
│                                                              │
│  Cliente A                    Cliente B                      │
│  ├─ NO valida                ├─ NO valida                  │
│  ├─ Envía TODO               ├─ Envía TODO                 │
│  └─ Confía en servidor       └─ Confía en servidor         │
│                                                              │
│              ▾                        ▾                      │
│         ┌────────────────────────────────┐                  │
│         │        SERVIDOR                │                  │
│         │  ✅ Única fuente de verdad     │                  │
│         │  ✅ Valida TODO                │                  │
│         │  ✅ Estado consistente         │                  │
│         │  ✅ Anti-trampas               │                  │
│         └────────────────────────────────┘                  │
│                                                              │
│  VENTAJAS:                                                   │
│  â€Ē Imposible hacer trampa (servidor controla todo)          │
│  â€Ē Todos los clientes ven el mismo grid                     │
│  â€Ē Un solo punto de validaciÃģn (mÃĄs fÃĄcil de mantener)      │
│  â€Ē Cliente mÃĄs simple (menos cÃģdigo, menos bugs)            │
│                                                              │
└─────────────────────────────────────────────────────────────┘

8. Optimizaciones y Consideraciones de Rendimiento

AnÃĄlisis de Memoria:

EstimaciÃģn de memoria para self.bombs:

TamaÃąo de tupla (x, y):
  â€Ē x: int (28 bytes en Python 3)
  â€Ē y: int (28 bytes en Python 3)
  â€Ē tupla overhead: ~40 bytes
  â€Ē Total por tupla: ~96 bytes

TamaÃąo del set:
  â€Ē Set overhead: ~232 bytes (tabla hash)
  â€Ē Por elemento: ~96 bytes
  
Grid mÃĄximo (Ronda 5: 14×14):
  â€Ē MÃĄximo de casillas: 14 × 14 = 196
  â€Ē Bombas típicas: ~30 (2 jugadores × 15 bombas)
  â€Ē Memoria: 232 + (30 × 96) = 3,112 bytes ≈ 3 KB

CONCLUSIÓN: Memoria insignificante incluso para grids grandes ✅

OptimizaciÃģn de BÚsqueda:

# ÂŋPor quÃĐ O(1) en vez de O(n)?

# Con lista (O(n)):
for bomb in self.bombs:  # Revisa CADA elemento
    if bomb == (x, y):
        return True
# Tiempo: n comparaciones

# Con set (O(1)):
hash_value = hash((x, y))      # 1 operaciÃģn
index = hash_value % table_size  # 1 operaciÃģn
return table[index] == (x, y)    # 1 comparaciÃģn
# Tiempo: 3 operaciones (constante)

ðŸŽŊ ConclusiÃģn TÃĐcnica

El sistema de detecciÃģn de bombas duplicadas es un ejemplo perfecto de ingeniería de software sÃģlida:

  1. Estructura de datos Ãģptima: Set con complejidad O(1)
  2. ValidaciÃģn centralizada: Servidor como fuente Única de verdad
  3. SincronizaciÃģn correcta: threading.Lock para evitar race conditions
  4. Arquitectura segura: Cliente no valida, imposible hacer trampa
  5. Eficiencia: Memoria mínima, velocidad mÃĄxima

Este diseÃąo garantiza que nunca habrÃĄ dos bombas en la misma casilla, independientemente de:

  • CuÃĄntos jugadores haya
  • QuÃĐ tan rÃĄpido hagan clic
  • Si intentan hacer trampa modificando el cliente
  • CuÃĄntas bombas se coloquen en total

La integridad del grid estÃĄ matemÃĄticamente garantizada. ✅


🔒 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:

# servidor.py:28-47 - SincronizaciÃģn con Lock
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 (Juego y Correo)
Servicios API OpenWeather, scraping de Wallapop, IMAP/SMTP
SincronizaciÃģn Locks para acceso concurrente a estado compartido

📧 CLIENTE DE CORREO ELECTRÓNICO: ARQUITECTURA Y FUNCIONAMIENTO

🏗ïļ Arquitectura del Sistema de Correo

┌─────────────────────────────────────────────────────────────────────┐
│                ARQUITECTURA DEL CLIENTE DE CORREO                    │
└─────────────────────────────────────────────────────────────────────┘

                    ┌───────────────────────────┐
                    │   SERVIDOR WEBMIN         │
                    │   10.10.0.101             │
                    └─────────────┮─────────────┘
                                  │
            ┌─────────────────────â”ī─────────────────────┐
            │                                           │
    ┌───────▾────────┐                        ┌────────▾────────┐
    │  PUERTO 143    │                        │   PUERTO 25     │
    │     IMAP       │                        │     SMTP        │
    │  (Lectura)     │                        │   (Envío)       │
    └───────┮────────┘                        └────────┮────────┘
            │                                           │
            └─────────────────┮───────────────────────┘
                              │
                    ┌─────────▾──────────┐
                    │   app.py           │
                    │   DashboardApp     │
                    │                    │
                    │  Tab "Correos"     │
                    └────────────────────┘
                              │
        ┌─────────────────────┾─────────────────────┐
        │                     │                     │
┌───────▾────────┐  ┌─────────▾────────┐  ┌────────▾────────┐
│ _connect_      │  │ _refresh_        │  │ _send_mail_     │
│ mail_server()  │  │ mail_list()      │  │ with_attach()   │
│                │  │                  │  │                 │
│ IMAP Login     │  │ IMAP FETCH       │  │ SMTP Send       │
└────────────────┘  └──────────────────┘  └─────────────────┘

ðŸ“Ą Protocolos Utilizados

IMAP (Internet Message Access Protocol)

Protocolo para leer correos del servidor. Puerto: 143 (sin TLS).

# app.py:1578-1582 - ConexiÃģn IMAP
import imaplib
self.imap_connection = imaplib.IMAP4(host, port_num)  # Puerto 143
self.imap_connection.login(username, password)

Comandos IMAP utilizados:

  • login(user, pass) → AutenticaciÃģn
  • select(mailbox) → Seleccionar carpeta (INBOX, Sent, etc.)
  • search(None, 'ALL') → Buscar todos los correos
  • fetch(id, '(BODY.PEEK[] FLAGS)') → Obtener correo sin marcarlo como leído
  • store(id, '+FLAGS', '\\Seen') → Marcar como leído
  • append(folder, flags, date, msg) → Guardar correo en carpeta

SMTP (Simple Mail Transfer Protocol)

Protocolo para enviar correos. Puerto: 25 (sin TLS).

# app.py:2949-2955 - ConexiÃģn y envío SMTP
import smtplib
with smtplib.SMTP(smtp_host, smtp_port_num, timeout=15) as server:
    server.send_message(msg, to_addrs=recipients)

🔐 Sistema de Credenciales

Guardado AutomÃĄtico con Base64

# app.py:1540-1557 - FunciÃģn _save_mail_credentials()
import base64
config = {
    'imap_host': '10.10.0.101',
    'imap_port': '143',
    'smtp_host': '10.10.0.101',
    'smtp_port': '25',
    'username': 'marcos@psp.es',
    'password': base64.b64encode(password.encode()).decode()  # Codificar
}
json.dump(config, open('.mail_config.json', 'w'), indent=2)
# app.py:1512-1520 - FunciÃģn _load_mail_credentials()
config = json.load(open('.mail_config.json', 'r'))
password = base64.b64decode(config['password']).decode()  # Decodificar

Flujo de guardado:

┌─────────────────────────────────────────────────────────────────────┐
│              FLUJO DE GUARDADO DE CREDENCIALES                       │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. Usuario ingresa credenciales                                    │
│     └─▹ Usuario: marcos@psp.es                                      │
│     └─▹ Password: 1234                                              │
│                                                                      │
│  2. Marca checkbox "ðŸ’ū Recordar credenciales"                       │
│     └─▹ mail_remember_var.get() = True                              │
│                                                                      │
│  3. Al conectar exitosamente, se llama _save_mail_credentials()     │
│                                                                      │
│  4. Base64 encoding:                                                │
│     Password "1234" → Bytes b'1234'                                 │
│                       → Base64 b'MTIzNA=='                          │
│                       → String "MTIzNA=="                           │
│                                                                      │
│  5. Se guarda en .mail_config.json:                                 │
│     {                                                               │
│       "username": "marcos@psp.es",                                  │
│       "password": "MTIzNA=="                                        │
│     }                                                               │
│                                                                      │
│  6. Al reiniciar app.py:                                            │
│     └─▹ _load_mail_credentials() lee el archivo                     │
│     └─▹ Decodifica Base64: "MTIzNA==" → "1234"                      │
│     └─▹ Precarga los campos automÃĄticamente                         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

⚠ïļ IMPORTANTE: Base64 NO es encriptaciÃģn, solo ofuscaciÃģn. El archivo .mail_config.json estÃĄ protegido en .gitignore para no subirlo a Git.


📎 Lectura de Correos (IMAP)

FunciÃģn Principal: _refresh_mail_list() (líneas 1635-1801)

┌─────────────────────────────────────────────────────────────────────┐
│                   FLUJO DE LECTURA DE CORREOS                        │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. SELECT 'INBOX'                                                  │
│     └─▹ imap_connection.select('INBOX')                             │
│                                                                      │
│  2. SEARCH ALL                                                      │
│     └─▹ status, messages = imap_connection.search(None, 'ALL')      │
│     └─▹ mail_ids = messages[0].split()  # [b'1', b'2', b'3', ...]  │
│                                                                      │
│  3. FETCH con BODY.PEEK[] (no marca como leído)                     │
│     for mail_id in mail_ids:                                        │
│         status, msg_data = imap_connection.fetch(                   │
│             mail_id,                                                │
│             '(BODY.PEEK[] FLAGS)'  # ← PEEK es clave               │
│         )                                                           │
│                                                                      │
│  4. EXTRAER FLAGS (\Seen)                                           │
│     └─▹ Busca en respuesta IMAP: "FLAGS (\\Seen ...)"               │
│     └─▹ is_seen = True si contiene "\\Seen"                         │
│                                                                      │
│  5. PARSEAR EMAIL                                                   │
│     └─▹ msg = email.message_from_bytes(msg_data)                    │
│     └─▹ from_addr = decode_header(msg['From'])[0][0]               │
│     └─▹ subject = decode_header(msg['Subject'])[0][0]              │
│                                                                      │
│  6. GUARDAR EN LISTA LOCAL                                          │
│     self.mail_list.append({                                         │
│         'id': mail_id,                                              │
│         'from': from_addr,                                          │
│         'subject': subject,                                         │
│         'date': date_str,                                           │
│         'msg': msg,  # Objeto email completo                        │
│         'is_seen': is_seen                                          │
│     })                                                              │
│                                                                      │
│  7. MOSTRAR EN LISTBOX                                              │
│     if is_seen:                                                     │
│         display = f'    {from_addr} - {subject}'  # Sin emoji       │
│         itemconfig(idx, fg='#888888')  # Gris                       │
│     else:                                                           │
│         display = f'ðŸ”ĩ {from_addr} - {subject}'  # Con emoji        │
│         itemconfig(idx, fg='#000000')  # Negro                      │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ÂŋPor quÃĐ BODY.PEEK[]?

# ❌ MAL: RFC822 marca el correo como leído automÃĄticamente
fetch(mail_id, 'RFC822')

# ✅ BIEN: BODY.PEEK[] lee sin cambiar el flag \Seen
fetch(mail_id, '(BODY.PEEK[] FLAGS)')

ðŸ“Ī Envío de Correos (SMTP)

FunciÃģn: _send_mail_with_attachments() (líneas 2837-2977)

┌─────────────────────────────────────────────────────────────────────┐
│                    FLUJO DE ENVÍO DE CORREO                          │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. VALIDAR DESTINATARIOS                                           │
│     to_addr_raw = "marcos@psp.es, user2@example.com"               │
│     └─▹ Split por comas/punto y coma                                │
│     └─▹ recipients = ['marcos@psp.es', 'user2@example.com']        │
│     └─▹ Validar regex: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,} │
│                                                                      │
│  2. CREAR MENSAJE MIME MULTIPART                                    │
│     from email.mime.multipart import MIMEMultipart                  │
│     msg = MIMEMultipart()                                           │
│     msg['From'] = 'marcos@psp.es'                                   │
│     msg['To'] = 'marcos@psp.es, user2@example.com'                 │
│     msg['Subject'] = 'Asunto del correo'                            │
│                                                                      │
│  3. ADJUNTAR CUERPO                                                 │
│     from email.mime.text import MIMEText                            │
│     msg.attach(MIMEText(body, 'plain', 'utf-8'))                    │
│                                                                      │
│  4. ADJUNTAR ARCHIVOS                                               │
│     for file_path in attachments:                                   │
│         if file_ext == '.png':                                      │
│             part = MIMEImage(file_data, name=filename)              │
│         elif file_ext == '.pdf':                                    │
│             part = MIMEApplication(file_data, _subtype='pdf')       │
│         # ... otros tipos                                           │
│         msg.attach(part)                                            │
│                                                                      │
│  5. CONECTAR SMTP Y ENVIAR                                          │
│     with smtplib.SMTP('10.10.0.101', 25) as server:                 │
│         server.send_message(msg, to_addrs=recipients)               │
│                                                                      │
│  6. GUARDAR EN CARPETA SENT (IMAP)                                  │
│     └─▹ _save_to_sent_folder(msg)  # Ver siguiente secciÃģn         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Tipos MIME Soportados

ExtensiÃģn Tipo MIME Clase Python
.png, .jpg image/* MIMEImage
.pdf application/pdf MIMEApplication
.doc, .docx application/msword MIMEApplication
.xls, .xlsx application/vnd.ms-excel MIMEApplication
.zip, .rar application/zip MIMEApplication
.txt text/plain MIMEText
Otros application/octet-stream MIMEBase

ðŸ’ū Guardado en Servidor IMAP

FunciÃģn: _save_to_sent_folder() (líneas 2979-3033)

DespuÃĐs de enviar un correo por SMTP, se guarda una copia en la carpeta "Sent" del servidor IMAP para que aparezca en Webmin y otros clientes.

# app.py:2990-3015 - Guardar correo en carpeta Sent del servidor
# Intentar mÚltiples nombres de carpeta
sent_folders = ['Sent', 'INBOX.Sent', 'Enviados', 'INBOX.Enviados', 'Sent Items']

for folder in sent_folders:
    try:
        # Intentar seleccionar la carpeta
        status, _ = self.imap_connection.select(folder)
        if status == 'OK':
            # Guardar correo con APPEND
            self.imap_connection.append(
                folder,
                '\\Seen',  # Marcar como leído
                imaplib.Time2Internaldate(time.time()),
                msg.as_bytes()
            )
            break
    except:
        continue

# Si no existe, crearla
if not folder_found:
    self.imap_connection.create('Sent')
    self.imap_connection.append('Sent', '\\Seen', date, msg_bytes)
┌─────────────────────────────────────────────────────────────────────┐
│              GUARDADO EN CARPETA SENT DEL SERVIDOR                   │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. Usuario envía correo con SMTP                                   │
│     └─▹ Correo enviado a marcos@psp.es                              │
│                                                                      │
│  2. _save_to_sent_folder() se ejecuta automÃĄticamente               │
│                                                                      │
│  3. Intenta conectar a carpeta "Sent":                              │
│     ├─▹ Intenta: SELECT 'Sent'           ❌ Falla                   │
│     ├─▹ Intenta: SELECT 'INBOX.Sent'     ✅ OK                      │
│     └─▹ Carpeta encontrada                                          │
│                                                                      │
│  4. Guarda el correo con APPEND:                                    │
│     APPEND "INBOX.Sent" (\Seen) "19-Feb-2026 19:30:00" {bytes}     │
│                                                                      │
│  5. Resultado:                                                      │
│     ┌─────────────────────────────────────┐                         │
│     │  SERVIDOR WEBMIN                    │                         │
│     │  ├─ INBOX (3 correos)               │                         │
│     │  └─ Sent (1 correo NUEVO) ← AQUÍ   │                         │
│     └─────────────────────────────────────┘                         │
│                                                                      │
│  6. Al abrir Webmin:                                                │
│     └─▹ El correo aparece en "Sent"                                 │
│     └─▹ Otros clientes (Thunderbird, etc.) tambiÃĐn lo ven           │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ðŸ‘Ĩ Envío a MÚltiples Destinatarios

ValidaciÃģn y Parsing

# app.py:2703-2740 - ValidaciÃģn de mÚltiples destinatarios
# Entrada del usuario
to_addr_raw = "marcos@psp.es, user2@example.com; user3@test.org"

# 1. Split por comas O punto y coma
import re
recipients = re.split(r'[;,]\s*', to_addr_raw)
# → ['marcos@psp.es', 'user2@example.com', 'user3@test.org']

# 2. Limpiar espacios
recipients = [r.strip() for r in recipients if r.strip()]

# 3. Validar formato con regex
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
invalid_emails = [email for email in recipients if not re.match(email_pattern, email)]

if invalid_emails:
    messagebox.showwarning('⚠ïļ Advertencia', 
        f'Los siguientes emails no son vÃĄlidos:\n{", ".join(invalid_emails)}')
    return

# 4. Confirmar si son mÚltiples
if len(recipients) > 1:
    confirm = messagebox.askyesno('📧 MÚltiples destinatarios',
        f'ÂŋEnviar correo a {len(recipients)} destinatarios?\n\n' + 
        '\n'.join(f'  â€Ē {email}' for email in recipients))
    if not confirm:
        return

# 5. Enviar a todos
self._send_mail_with_attachments(recipients, subject, body, attachments)
┌─────────────────────────────────────────────────────────────────────┐
│                ENVÍO A MÚLTIPLES DESTINATARIOS                       │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  Usuario escribe en campo "Para:":                                  │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ marcos@psp.es, user2@test.com, user3@example.org           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  Al hacer clic en "ðŸ“Ī ENVIAR":                                      │
│                                                                      │
│  1. Parse: Split por ',' o ';'                                      │
│     ['marcos@psp.es', 'user2@test.com', 'user3@example.org']       │
│                                                                      │
│  2. ValidaciÃģn regex de cada email                                  │
│     ✅ marcos@psp.es       → VÃĄlido                                 │
│     ✅ user2@test.com      → VÃĄlido                                 │
│     ✅ user3@example.org   → VÃĄlido                                 │
│                                                                      │
│  3. DiÃĄlogo de confirmaciÃģn:                                        │
│     ┌───────────────────────────────────┐                           │
│     │ ÂŋEnviar a 3 destinatarios?        │                           │
│     │                                   │                           │
│     │  â€Ē marcos@psp.es                  │                           │
│     │  â€Ē user2@test.com                 │                           │
│     │  â€Ē user3@example.org              │                           │
│     │                                   │                           │
│     │     [Sí]        [No]              │                           │
│     └───────────────────────────────────┘                           │
│                                                                      │
│  4. SMTP envía a todos:                                             │
│     msg['To'] = 'marcos@psp.es, user2@test.com, user3@example.org' │
│     server.send_message(msg, to_addrs=[...])                        │
│                                                                      │
│  5. Mensaje de ÃĐxito:                                               │
│     ✅ "Correo enviado correctamente a 3 destinatarios"             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

📎 Manejo de Adjuntos

Adjuntar Archivos

# app.py:2568-2590 - Adjuntar archivos con diÃĄlogo
# Usuario hace clic en "📎 Adjuntar archivo"
file_paths = filedialog.askopenfilenames(
    title='Seleccionar archivos',
    filetypes=[
        ('ImÃĄgenes', '*.png *.jpg *.jpeg *.gif'),
        ('PDFs', '*.pdf'),
        ('Documentos', '*.doc *.docx *.xls *.xlsx'),
        ('Todos', '*.*')
    ]
)

# Se guardan en lista
attachments.append(file_path)

# Al enviar, se procesan (app.py:2895-2920):
for file_path in attachments:
    file_name = os.path.basename(file_path)  # "documento.pdf"
    file_ext = os.path.splitext(file_path)[1]  # ".pdf"
    
    with open(file_path, 'rb') as f:
        file_data = f.read()
    
    if file_ext == '.pdf':
        part = MIMEApplication(file_data, _subtype='pdf')
        part.add_header('Content-Disposition', 'attachment', filename=file_name)
        msg.attach(part)

ImÃĄgenes Inline con Ctrl+V

# app.py:2609-2645 - Pegar imagen desde portapapeles
# Usuario copia una imagen y presiona Ctrl+V
def on_paste(event):
    try:
        # Obtener imagen del portapapeles
        img = ImageGrab.grabclipboard()
        
        if img:
            # Guardar temporalmente
            temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
            img.save(temp_file.name)
            
            # Mostrar miniatura en interfaz
            thumbnail = img.resize((150, 150))
            photo = ImageTk.PhotoImage(thumbnail)
            label = tk.Label(frame, image=photo)
            label.image = photo  # Mantener referencia
            label.pack()
            
            # Agregar a lista de adjuntos
            inline_images_data.append({'data': img_bytes})
    except:
        pass

🖞ïļ VisualizaciÃģn de Correos

FunciÃģn: _display_mail() (líneas 2055-2337)

┌─────────────────────────────────────────────────────────────────────┐
│                  VISUALIZACIÓN DE CORREO                             │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. Usuario hace clic en un correo de la lista                      │
│     └─▹ _on_mail_select() → _display_mail(mail_info)                │
│                                                                      │
│  2. Marcar como leído en servidor (si es INBOX y no leído)          │
│     if not mail_info['is_seen']:                                    │
│         imap_connection.store(mail_id, '+FLAGS', '\\Seen')          │
│         # Actualizar visualmente: quitar ðŸ”ĩ, poner gris             │
│                                                                      │
│  3. Actualizar encabezados                                          │
│     mail_from_label.config(text='De: marcos@psp.es')                │
│     mail_subject_label.config(text='Asunto: Test')                  │
│     mail_date_label.config(text='Fecha: 19/02/2026')                │
│                                                                      │
│  4. Procesar contenido multipart                                    │
│     if msg.is_multipart():                                          │
│         for part in msg.walk():                                     │
│             if content_type == 'text/plain':                        │
│                 body = part.get_payload(decode=True).decode()       │
│             elif part.get_filename():  # Adjunto                    │
│                 attachments.append({...})                           │
│                                                                      │
│  5. Mostrar cuerpo en Text widget                                   │
│     mail_body_text.delete('1.0', 'end')                             │
│     mail_body_text.insert('1.0', body)                              │
│                                                                      │
│  6. Mostrar imÃĄgenes inline (si PIL estÃĄ disponible)                │
│     for att in images:                                              │
│         img = Image.open(BytesIO(att['data']))                      │
│         img.thumbnail((500, 500))  # Redimensionar                  │
│         photo = ImageTk.PhotoImage(img)                             │
│         mail_body_text.image_create('end', image=photo)             │
│                                                                      │
│  7. Mostrar otros adjuntos (PDFs, docs, etc.)                       │
│     for att in other_attachments:                                   │
│         # Frame con icono, nombre, tamaÃąo y botÃģn "ðŸ’ū Guardar"      │
│         icon = '📄' if PDF else '📝' if Word else '📎'              │
│         Button(text='ðŸ’ū Guardar', command=save_file)                │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ðŸ”ĩ Sistema de Indicadores Visuales

# app.py:1775-1790 - Indicadores visuales de correos leídos/no leídos
# Al cargar correos
for mail in mail_list:
    if is_seen:
        # Correo leído
        display_text = f'    {from_addr[:27]} - {subject[:37]}'
        self.mail_listbox.insert('end', display_text)
        idx = self.mail_listbox.size() - 1
        self.mail_listbox.itemconfig(idx, 
            fg='#888888',           # Gris
            selectforeground='#666666'
        )
    else:
        # Correo NO leído
        display_text = f'ðŸ”ĩ {from_addr[:27]} - {subject[:37]}'
        self.mail_listbox.insert('end', display_text)
        idx = self.mail_listbox.size() - 1
        self.mail_listbox.itemconfig(idx, 
            fg='#000000',           # Negro
            selectforeground='#1a73e8'
        )

# Contador de no leídos
self.unread_count = sum(1 for m in self.mail_list if not m.get('is_seen', False))
self.unread_label.config(text=f'Correos sin leer: {self.unread_count}')
┌─────────────────────────────────────────────────────────────────────┐
│                  APARIENCIA VISUAL EN LISTBOX                        │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ 📎 Bandeja de entrada                    Correos sin leer: 2  │  │
│  ├───────────────────────────────────────────────────────────────â”Ī  │
│  │                                                               │  │
│  │ ðŸ”ĩ marcos@psp.es - Test para grabaciÃģn   ← NO LEÍDO (negro)  │  │
│  │ ðŸ”ĩ user@example.com - Propuesta proyecto ← NO LEÍDO (negro)  │  │
│  │     admin@server.com - NotificaciÃģn     ← LEÍDO (gris)       │  │
│  │     webmaster@test.org - Informe        ← LEÍDO (gris)       │  │
│  │                                                               │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                      │
│  Al hacer clic en el primero (ðŸ”ĩ NO LEÍDO):                         │
│  1. Se marca como leído en el servidor (STORE +FLAGS \Seen)         │
│  2. Se actualiza la visualizaciÃģn:                                  │
│     ├─ Quita el emoji ðŸ”ĩ                                            │
│     ├─ Cambia color a gris                                          │
│     └─ Decrementa contador: "Correos sin leer: 1"                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ðŸ›Ąïļ Manejo de Errores y Log

# app.py:4304-4315 - FunciÃģn _log() con verificaciÃģn de widget
def _log(self, text: str) -> None:
    # Verificar si estamos en hilo principal
    if threading.current_thread() is not threading.main_thread():
        self.after(0, lambda t=text: self._log(t))
        return
    
    # Verificar si el widget notes existe
    if not hasattr(self, 'notes') or self.notes is None:
        print(f'[LOG] {text}')  # Consola durante inicializaciÃģn
        return
    
    # Log normal en interfaz
    timestamp = datetime.datetime.now().strftime('%H:%M:%S')
    self.notes.insert('end', f'[{timestamp}] {text}\n')
    self.notes.see('end')
┌─────────────────────────────────────────────────────────────────────┐
│                     EJEMPLO DE LOG EN INTERFAZ                       │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 📝 Panel de Notas                                           │   │
│  ├─────────────────────────────────────────────────────────────â”Ī   │
│  │ [19:28:43] Conectando a 10.10.0.101:143...                 │   │
│  │ [19:28:44] ConexiÃģn IMAP establecida                        │   │
│  │ [19:28:44] Carpetas IMAP disponibles: ['INBOX', 'Sent']    │   │
│  │ [19:28:44] Credenciales guardadas correctamente            │   │
│  │ [19:28:45] Cargando 8 correos...                           │   │
│  │ [19:28:46] 8 correos cargados (2 sin leer)                 │   │
│  │ [19:29:10] === CORREO SELECCIONADO #0: Test grabaciÃģn ===  │   │
│  │ [19:29:10] Correo marcado como leído en el servidor        │   │
│  │ [19:29:10] >>> Actualizando encabezados                    │   │
│  │ [19:29:10] Texto plano encontrado: 245 caracteres          │   │
│  │ [19:29:10] Adjunto detectado: imagen.png (image/png)       │   │
│  │ [19:29:10] Imagen inline mostrada: imagen.png              │   │
│  │ [19:29:10] >>> _display_mail COMPLETADO OK                 │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

📊 Resumen de Funciones Clave

FunciÃģn Líneas Responsabilidad
_build_tab_correos() 632-850 Construir toda la interfaz del tab Correos
_load_mail_credentials() 1483-1525 Cargar credenciales de .mail_config.json
_save_mail_credentials() 1527-1559 Guardar credenciales con Base64
_connect_mail_server() 1561-1613 Conectar a IMAP, listar carpetas
_refresh_mail_list() 1635-1801 Cargar correos de INBOX con FETCH
_show_inbox() 1803-1811 Cambiar a bandeja de entrada
_show_sent() 1828-1880 Cambiar a carpeta de enviados
_on_mail_select() 1982-2053 Manejar clic en correo, marcar como leído
_display_mail() 2055-2337 Mostrar contenido, imÃĄgenes y adjuntos
_open_compose_window() 2353-2788 Abrir ventana de redacciÃģn
_send_mail_with_attachments() 2837-2977 Enviar correo por SMTP con adjuntos
_save_to_sent_folder() 2979-3033 Guardar copia en servidor IMAP

🔄 Flujo Completo de Usuario

┌─────────────────────────────────────────────────────────────────────┐
│               FLUJO COMPLETO: LEER Y ENVIAR CORREO                   │
├─────────────────────────────────────────────────────────────────────â”Ī
│                                                                      │
│  1. INICIAR APLICACIÓN                                              │
│     └─▹ python3 app.py                                              │
│     └─▹ _load_mail_credentials() precarga usuario y contraseÃąa     │
│                                                                      │
│  2. IR A TAB "CORREOS"                                              │
│     └─▹ _build_tab_correos() ya construyÃģ la interfaz              │
│                                                                      │
│  3. CONECTAR                                                        │
│     └─▹ Clic en "🔗 Conectar"                                       │
│     └─▹ _connect_mail_server()                                      │
│         ├─ IMAP4('10.10.0.101', 143)                               │
│         ├─ login('marcos@psp.es', '1234')                          │
│         └─ list() → Muestra carpetas disponibles                   │
│     └─▹ _save_mail_credentials() si "Recordar" estÃĄ marcado        │
│     └─▹ _refresh_mail_list() carga correos automÃĄticamente         │
│                                                                      │
│  4. LEER CORREO                                                     │
│     └─▹ Clic en correo de la lista                                 │
│     └─▹ _on_mail_select()                                           │
│         ├─ store(id, '+FLAGS', '\\Seen') si no leído              │
│         └─ _display_mail(mail_info)                                 │
│             ├─ Actualiza encabezados                                │
│             ├─ Muestra cuerpo                                       │
│             ├─ Muestra imÃĄgenes inline                              │
│             └─ Muestra botones para guardar adjuntos                │
│                                                                      │
│  5. ENVIAR NUEVO CORREO                                             │
│     └─▹ Clic en "✉ïļ Nuevo correo"                                  │
│     └─▹ _open_compose_window()                                      │
│         ├─ Ventana emergente con campos                             │
│         ├─ BotÃģn "📎 Adjuntar archivo"                             │
│         ├─ Soporte Ctrl+V para imÃĄgenes                            │
│         └─ BotÃģn "ðŸ“Ī ENVIAR CORREO"                                │
│             ├─ Validar destinatarios (regex)                        │
│             ├─ Confirmar si mÚltiples                               │
│             ├─ _send_mail_with_attachments()                        │
│             │   ├─ Crear MIMEMultipart                              │
│             │   ├─ Adjuntar archivos                                │
│             │   └─ SMTP send_message()                              │
│             └─ _save_to_sent_folder()                               │
│                 └─ IMAP APPEND a 'Sent'                             │
│                                                                      │
│  6. VERIFICAR EN WEBMIN                                             │
│     └─▹ Abrir http://10.10.0.101:20000                             │
│     └─▹ Carpeta "Sent" → Correo aparece ahí                        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

ðŸŽĻ 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