diff --git a/README.md b/README.md index 5755969..ac7738d 100644 --- a/README.md +++ b/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