fix: reset grid column and row configurations before re-configuring and update README.
This commit is contained in:
parent
afac3609cb
commit
f0dc49b62e
250
README.md
250
README.md
|
|
@ -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
|
## 🛠️ Tecnologías
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|
|
||||||
7
app.py
7
app.py
|
|
@ -1030,8 +1030,13 @@ class DashboardApp(tk.Tk):
|
||||||
# Limpiar grid anterior
|
# Limpiar grid anterior
|
||||||
for child in self.game_frame.winfo_children():
|
for child in self.game_frame.winfo_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
|
|
||||||
|
# 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
|
# Configurar grid nuevo
|
||||||
for i in range(size):
|
for i in range(size):
|
||||||
self.game_frame.columnconfigure(i, weight=1, uniform='cell')
|
self.game_frame.columnconfigure(i, weight=1, uniform='cell')
|
||||||
self.game_frame.rowconfigure(i, weight=1, uniform='cell')
|
self.game_frame.rowconfigure(i, weight=1, uniform='cell')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue