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 ```bash git clone https://github.com/MarcosFerrandiz/Proyecto1AVApsp.git cd Proyecto1AVApsp ``` ### Paso 2: Crear Entorno Virtual (Recomendado) ```bash python -m venv .venv # Linux/macOS source .venv/bin/activate # Windows .venv\Scripts\activate ``` ### Paso 3: Instalar Dependencias ```bash pip install -r requirements.txt ```
๐Ÿ“ฆ 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 ```bash python servidor.py ``` > El servidor escucharรก en `0.0.0.0:3333` ### ๐ŸŽฎ Opciรณn A: Dashboard Completo ```bash python app.py ``` Incluye el juego integrado + todas las funcionalidades del panel. ### ๐ŸŽฎ Opciรณn B: Cliente Standalone ```bash python cliente_juego.py ``` Cliente ligero solo para jugar al Minesweeper. ### ๐Ÿ”Œ Conectar al Juego 1. Introduce el **Host** del servidor (por defecto: `127.0.0.1`) 2. Verifica el **Puerto** (por defecto: `3333`) 3. Pulsa **"Conectar"** 4. ยกEspera a otro jugador y pulsa **"Iniciar Juego"**! --- ## ๐Ÿ’ฃ Mecรกnicas del Juego ### ๐Ÿ”„ Flujo de una Partida ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 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 ```javascript // Iniciar partida { "type": "START_GAME" } // Colocar bomba (fase PLACING) { "type": "PLACE_BOMB", "x": 2, "y": 1 } // Excavar casilla (fase PLAYING) { "type": "CLICK_CELL", "x": 3, "y": 4 } // Verificar zona despejada { "type": "CHECK_DUNGEON_CLEARED" } ``` ### Mensajes JSON Servidor โ†’ Clientes ```javascript // Nueva ronda { "type": "NEW_ROUND", "round": 1, "grid_size": 3, "total_bombs_per_player": 3 } // Notificaciรณn de turno { "type": "TURN_NOTIFY", "active_player": "('127.0.0.1', 54321)", "msg": "Turno de ... para poner bombas." } // Flash de bomba (visible 1 segundo) { "type": "BOMB_flash", "x": 1, "y": 2, "who": "('127.0.0.1', 54321)" } // Explosiรณn { "type": "EXPLOSION", "x": 1, "y": 2, "who": "...", "lives": 2 } // Casilla segura { "type": "SAFE", "x": 0, "y": 0 } // Game Over { "type": "GAME_OVER", "loser": "...", "msg": "๐Ÿ’€ ยก... ha perdido todas sus vidas!" } ``` --- ## ๐Ÿ”ง 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? ```python # 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: ```python # 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 ```python # 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: ```python # 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()`** ```python # 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** ```python # 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 | ```python # 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** ```python # 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:** ```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:** ```python # 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:** ```python # โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” # 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: ```python # 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:** ```python # 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:** ```python # ยฟ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: ```python # 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). ```python # 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). ```python # 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** ```python # 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) ``` ```python # 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[]?** ```python # โŒ 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. ```python # 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** ```python # 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** ```python # 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** ```python # 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 ```python # 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 ```python # 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