fix: reset grid column and row configurations before re-configuring and update README.

This commit is contained in:
marcos 2026-02-02 17:45:43 +01:00
parent afac3609cb
commit f0dc49b62e
2 changed files with 256 additions and 1 deletions

250
README.md
View File

@ -339,6 +339,256 @@ Cliente ligero solo para jugar al Minesweeper.
---
## 🔧 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 - 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 (servidor.py)
```python
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 - Al conectarse
self.my_address = str(sock.getsockname()) # Ej: "('127.0.0.1', 49956)"
# Al recibir TURN_NOTIFY del servidor
def handle_message(self, msg):
if mtype == 'TURN_NOTIFY':
active_player = msg.get('active_player') # "('127.0.0.1', 49956)"
# Comparar con mi dirección
if active_player == self.my_address:
self._log_game("🎯 ¡ES TU TURNO!")
# Habilitar controles...
else:
self._log_game(f"⏳ Turno de {active_player}")
# Deshabilitar controles...
```
```
┌─────────────────────────────────────────────────────────────────┐
│ FLUJO DE NOTIFICACIÓN DE TURNO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SERVIDOR CLIENTES │
│ ──────── ──────── │
│ │
│ broadcast({ Cliente 1 (49956): │
│ "type": "TURN_NOTIFY", ├─ my_address = 49956 │
│ "active_player": ├─ active_player = 49956 │
│ "('127.0.0.1', 49956)" └─ ✓ ¡ES MI TURNO! │
│ }) │
│ │ Cliente 2 (49968): │
│ └──────────────────────────► ├─ my_address = 49968 │
│ ├─ active_player = 49956 │
│ └─ ✗ No es mi turno │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
### 🔒 Sincronización con Threading Lock
El servidor usa un `threading.Lock` para evitar condiciones de carrera cuando múltiples clientes envían mensajes simultáneamente:
```python
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
<table>

7
app.py
View File

@ -1031,7 +1031,12 @@ class DashboardApp(tk.Tk):
for child in self.game_frame.winfo_children():
child.destroy()
# Configurar grid
# Resetear configuraciones de filas/columnas anteriores (máximo 14x14)
for i in range(14):
self.game_frame.columnconfigure(i, weight=0, uniform='')
self.game_frame.rowconfigure(i, weight=0, uniform='')
# Configurar grid nuevo
for i in range(size):
self.game_frame.columnconfigure(i, weight=1, uniform='cell')
self.game_frame.rowconfigure(i, weight=1, uniform='cell')