diff --git a/README.md b/README.md index 2af63dc..028becb 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,226 @@ def handle_message(self, msg): --- +### πŸ’£ 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 - LΓ­nea 32 +class GameServer: + def __init__(self): + self.bombs = set() # Set de tuplas (x, y) +``` + +Un `set()` en Python **no permite elementos duplicados**. Si intentas agregar la misma tupla `(x, y)` dos veces, solo se guarda una vez. + +#### **ValidaciΓ³n al Colocar Bomba (servidor.py lΓ­neas 222-246)** + +```python +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 (app.py lΓ­neas 1310-1330)** + +```python +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**. + +--- + ### πŸ”’ 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: