Proyecto1AVApsp/servidor.py

371 lines
15 KiB
Python

#!/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()