#!/usr/bin/env python3 """ Servidor de juego Minesweeper Multiplayer Protocolo basado en JSON sobre TCP. """ import socket import threading import json import time import random HOST = '0.0.0.0' PORT = 3333 # Estados del juego STATE_LOBBY = 'LOBBY' STATE_PLACING = 'PLACING' # Jugadores ponen bombas por turnos STATE_PLAYING = 'PLAYING' # Jugadores buscan class GameServer: def __init__(self): self.clients = {} # socket -> address self.client_list = [] # List of addresses to maintain order self.lock = threading.Lock() # Estado del juego self.state = STATE_LOBBY self.round = 1 self.lives = {} # address -> int self.grid_size = 3 self.bombs = set() # Set of coordinates (x, y) self.revealed = set() # Set of coordinates (x, y) # Turn control for placing self.placing_turn_index = 0 self.bombs_to_place_per_player = 0 self.current_player_bombs_placed = 0 # Turn control for playing (search phase) self.playing_turn_index = 0 def _broadcast_unlocked(self, message_dict): """Envía mensaje JSON a todos. NOTA: Debe llamarse con self.lock ya adquirido""" data = (json.dumps(message_dict) + '\n').encode('utf-8') print(f"[BROADCAST] Enviando {len(data)} bytes a {len(self.clients)} clientes") for client in list(self.clients.keys()): try: client.sendall(data) print(f"[BROADCAST] ✓ Enviado a {self.clients[client]}") except Exception as e: print(f"[BROADCAST] ✗ Error enviando a {self.clients.get(client)}: {e}") # No llamar remove_client aquí porque ya tenemos el lock # Solo marcar para remover después def broadcast(self, message_dict): """Envía mensaje JSON a todos con newline (adquiere lock)""" with self.lock: self._broadcast_unlocked(message_dict) def remove_client(self, client): if client in self.clients: addr = self.clients[client] del self.clients[client] if addr in self.client_list: self.client_list.remove(addr) print(f"[DESCONECTADO] {addr}") # Si no quedan jugadores, reiniciar if not self.clients: self.reset_game() def reset_game(self): print("Reiniciando juego...") self.state = STATE_LOBBY self.round = 1 self.lives = {} self.bombs = set() self.revealed = set() self.client_list = [] def get_grid_size(self): # Tamaños de grid por ronda (máximo 14x14) if self.round == 1: return 3 if self.round == 2: return 5 if self.round == 3: return 8 if self.round == 4: return 11 return 14 # Ronda 5+ def get_bombs_per_player(self): # Bombas por ronda: 3, 5, 9, 12, 15 if self.round == 1: return 3 if self.round == 2: return 5 if self.round == 3: return 9 if self.round == 4: return 12 return 15 # Ronda 5+ def start_round(self): """Inicia una nueva ronda. NOTA: Debe llamarse con self.lock ya adquirido""" self.grid_size = self.get_grid_size() self.bombs = set() self.revealed = set() self.state = STATE_PLACING self.placing_turn_index = 0 self.current_player_bombs_placed = 0 self.bombs_to_place_per_player = self.get_bombs_per_player() # Actualizar lista de clientes para turnos (lock ya adquirido por caller) self.client_list = list(self.clients.values()) print(f"[ROUND {self.round}] Grid: {self.grid_size}x{self.grid_size}, Bombas/jugador: {self.bombs_to_place_per_player}") print(f"[ROUND {self.round}] Jugadores: {self.client_list}") msg = { "type": "NEW_ROUND", "round": self.round, "grid_size": self.grid_size, "status": f"Ronda {self.round}: Fase de Colocación", "total_bombs_per_player": self.bombs_to_place_per_player } print(f"[BROADCAST] NEW_ROUND: {msg}") self._broadcast_unlocked(msg) self.next_placement_turn() def start_round_with_lock(self): """Versión de start_round que adquiere el lock (para usar con Timer)""" with self.lock: self.start_round() def next_placement_turn(self): print(f"[TURN] Index: {self.placing_turn_index}, Total jugadores: {len(self.client_list)}") if self.placing_turn_index >= len(self.client_list): # Todos han puesto bombas - iniciar fase de búsqueda print("[PHASE] Cambiando a PLAYING") self.state = STATE_PLAYING self.playing_turn_index = 0 # Empieza el primer jugador self._broadcast_unlocked({ "type": "PHASE_PLAY", "msg": "¡A buscar! Recordad dónde estaban las bombas." }) # Notificar turno del primer jugador self.notify_playing_turn() return current_addr = self.client_list[self.placing_turn_index] self.current_player_bombs_placed = 0 turn_msg = { "type": "TURN_NOTIFY", "active_player": str(current_addr), "msg": f"Turno de {current_addr} para poner bombas." } print(f"[BROADCAST] TURN_NOTIFY: {turn_msg}") self._broadcast_unlocked(turn_msg) def notify_playing_turn(self): """Notifica el turno actual en la fase de búsqueda""" if self.playing_turn_index >= len(self.client_list): self.playing_turn_index = 0 # Volver al principio current_addr = self.client_list[self.playing_turn_index] turn_msg = { "type": "SEARCH_TURN", "active_player": str(current_addr), "msg": f"🔍 Turno de {current_addr} para excavar." } print(f"[SEARCH_TURN] Jugador {self.playing_turn_index}: {current_addr}") self._broadcast_unlocked(turn_msg) def handle_client(self, client_sock, addr): print(f"[CONECTADO] {addr}") with self.lock: self.clients[client_sock] = addr if addr not in self.lives: self.lives[addr] = 3 try: while True: data = client_sock.recv(4096) if not data: break try: text_chunk = data.decode('utf-8', errors='replace') lines = text_chunk.split('\n') for line in lines: line = line.strip() if not line: continue try: msg = json.loads(line) self.process_message(client_sock, addr, msg) except json.JSONDecodeError: pass except Exception as e: print(f"Error decode {addr}: {e}") except Exception as e: print(f"Error {addr}: {e}") finally: with self.lock: self.remove_client(client_sock) def process_message(self, client, addr, msg): msg_type = msg.get('type') print(f"[MSG] {addr}: {msg_type}") # Debug log with self.lock: if msg_type == 'START_GAME': print(f"[START_GAME] Estado actual: {self.state}, Clientes: {len(self.clients)}") if self.state == STATE_LOBBY: # Reiniciar juego pero mantener clientes self.round = 1 self.bombs = set() self.revealed = set() # Re-registrar vidas para los presentes self.lives = {} for c_addr in self.clients.values(): self.lives[c_addr] = 3 print(f"[START_GAME] Iniciando ronda 1 con {len(self.clients)} jugadores") self.start_round() else: print(f"[START_GAME] Ignorado - estado: {self.state}") elif msg_type == 'PLACE_BOMB': if self.state == STATE_PLACING: # Verificar turno current_turn_addr = self.client_list[self.placing_turn_index] if str(addr) != str(current_turn_addr): return # No es su turno x, y = msg['x'], msg['y'] # Evitar poner bomba donde ya hay (opcional, o permite solapar) if (x,y) in self.bombs: return # Ya hay bomba aqui self.bombs.add((x, y)) self.current_player_bombs_placed += 1 # FLASH: Mostrar bomba 1 segundo self._broadcast_unlocked({ "type": "BOMB_flash", "x": x, "y": y, "who": str(addr) }) if self.current_player_bombs_placed >= self.bombs_to_place_per_player: self.placing_turn_index += 1 self.next_placement_turn() elif msg_type == 'CLICK_CELL': if self.state == STATE_PLAYING: # Verificar turno current_turn_addr = self.client_list[self.playing_turn_index] if str(addr) != str(current_turn_addr): print(f"[CLICK_CELL] Ignorado - no es el turno de {addr}") return # No es su turno x, y = msg['x'], msg['y'] # Verificar si ya fue revelada if (x, y) in self.revealed: print(f"[CLICK_CELL] Celda {x},{y} ya revelada") return # Ya revelada, no contar como turno if (x, y) in self.bombs: # BOOM logic - el jugador pierde una vida self.lives[addr] -= 1 # Notificar explosión a todos self._broadcast_unlocked({ "type": "EXPLOSION", "x": x, "y": y, "who": str(addr), "lives": self.lives[addr], "player_addr": str(addr) }) # LÓGICA DE AVANCE DE RONDA # Verificar si el jugador perdió todas sus vidas if self.lives[addr] <= 0: # ¡Este jugador perdió! El otro gana self._broadcast_unlocked({ "type": "GAME_OVER", "loser": str(addr), "msg": f"💀 ¡{addr} ha perdido todas sus vidas! ¡GAME OVER!" }) self.reset_game() return # Si estamos en ronda 5 o más, repetir la ronda if self.round >= 5: self._broadcast_unlocked({ "type": "ROUND_ADVANCE", "msg": f"💥 ¡Explosión! Repitiendo ronda final..." }) # Repetir misma ronda (no incrementar) threading.Timer(3.0, self.start_round_with_lock).start() else: # Avanzar a siguiente ronda self.round += 1 self._broadcast_unlocked({ "type": "ROUND_ADVANCE", "msg": f"💥 ¡Explosión! Pasando a ronda {self.round}..." }) threading.Timer(3.0, self.start_round_with_lock).start() return # No avanzar turno, la ronda terminó else: # Safe self.revealed.add((x, y)) self._broadcast_unlocked({ "type": "SAFE", "x": x, "y": y }) # Avanzar turno self.playing_turn_index += 1 if self.playing_turn_index >= len(self.client_list): self.playing_turn_index = 0 self.notify_playing_turn() elif msg_type == 'CHECK_DUNGEON_CLEARED': # Solo puede verificar el jugador que tiene el turno if self.state == STATE_PLAYING: current_turn_addr = self.client_list[self.playing_turn_index] if str(addr) != str(current_turn_addr): self._broadcast_unlocked({ "type": "WARNING", "msg": "🛑 ¡Solo el jugador activo puede reclamar victoria!" }) return # Validar bombs_exploded = 0 # No trackeamos exploded aparte, pero asumimos que siguen en self.bombs # Logic: Si quedan celdas NO reveladas y NO son bombas -> Faltan cosas # Grid total total_cells = self.grid_size * self.grid_size safe_cells_count = total_cells - len(self.bombs) # Count safely revealed # revealed set only contains SAFE cells based on logic above if len(self.revealed) >= safe_cells_count: # Win Round self._broadcast_unlocked({"type": "ROUND_WIN", "msg": "¡Zona despejada!"}) self.round += 1 if self.round > 5: self._broadcast_unlocked({"type": "GAME_WIN", "msg": "¡JUEGO COMPLETADO!"}) self.reset_game() else: threading.Timer(3.0, self.start_round).start() else: self._broadcast_unlocked({ "type": "WARNING", "msg": "¡Aún hay zonas sin explorar!" }) def start_server(): server = GameServer() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST, PORT)) s.listen(5) print(f"Servidor Juego escuchando en {HOST}:{PORT}") while True: conn, addr = s.accept() t = threading.Thread(target=server.handle_client, args=(conn, addr), daemon=True) t.start() if __name__ == '__main__': start_server()