#!/usr/bin/env python3 import tkinter as tk from tkinter import messagebox, scrolledtext, simpledialog import socket import threading import json import time import queue SERVER_HOST = '127.0.0.1' SERVER_PORT = 3333 class GameClient: def __init__(self, root): self.root = root self.root.title("Minesweeper Multiplayer - Cliente Dedicado") self.root.geometry("600x700") # Conexión self.sock = None self.connected = False self.msg_queue = queue.Queue() # Estado UI self.buttons = {} self.game_phase = 'LOBBY' self.build_ui() # Loop de mensajes UI self.root.after(100, self.process_queue) def build_ui(self): # Frame Superior (Conexión) conn_frame = tk.Frame(self.root, pady=5) conn_frame.pack(fill='x', padx=10, pady=5) tk.Label(conn_frame, text="Host:").pack(side='left') self.ent_host = tk.Entry(conn_frame, width=15) self.ent_host.insert(0, SERVER_HOST) self.ent_host.pack(side='left', padx=5) tk.Label(conn_frame, text="Port:").pack(side='left') self.ent_port = tk.Entry(conn_frame, width=6) self.ent_port.insert(0, str(SERVER_PORT)) self.ent_port.pack(side='left', padx=5) self.btn_connect = tk.Button(conn_frame, text="Conectar", command=self.connect) self.btn_connect.pack(side='left', padx=10) self.btn_start = tk.Button(conn_frame, text="Iniciar Juego", command=self.start_game, bg='#90ee90', state='disabled') self.btn_start.pack(side='right', padx=10) # Frame Stats stats_frame = tk.Frame(self.root, pady=5) stats_frame.pack(fill='x', padx=20) self.lbl_round = tk.Label(stats_frame, text="Ronda: -", font=('Arial', 14)) self.lbl_round.pack(side='left') self.lbl_lives = tk.Label(stats_frame, text="Vidas: -", font=('Arial', 14, 'bold'), fg='red') self.lbl_lives.pack(side='right') # Frame Juego self.game_frame = tk.Frame(self.root, bg='#cccccc') self.game_frame.pack(fill='both', expand=True, padx=20, pady=10) # Logs self.log_area = scrolledtext.ScrolledText(self.root, height=8) self.log_area.pack(fill='x', padx=10, pady=10) # Botones Control ctrl_frame = tk.Frame(self.root) ctrl_frame.pack(pady=5) self.btn_done = tk.Button(ctrl_frame, text="¡Zona Limpia!", command=self.check_cleared, bg='gold', state='disabled') self.btn_done.pack() def log(self, text): self.log_area.insert('end', f"> {text}\n") self.log_area.see('end') def connect(self): host = self.ent_host.get() try: port = int(self.ent_port.get()) except ValueError: messagebox.showerror("Error", "Puerto inválido") return try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) self.connected = True threading.Thread(target=self.recv_loop, daemon=True).start() self.log(f"Conectado a {host}:{port}") self.btn_connect.config(state='disabled') self.btn_start.config(state='normal') except Exception as e: messagebox.showerror("Error Conexión", str(e)) def send(self, data): if not self.connected or not self.sock: return try: msg = json.dumps(data) + '\n' self.sock.sendall(msg.encode('utf-8')) except Exception as e: self.log(f"Error enviando: {e}") def recv_loop(self): while self.connected: try: data = self.sock.recv(4096) if not data: break text = data.decode('utf-8', errors='replace') for line in text.split('\n'): line = line.strip() if not line: continue try: self.msg_queue.put(json.loads(line)) except: pass except: break self.connected = False self.msg_queue.put({"type": "DISCONNECT"}) def process_queue(self): while not self.msg_queue.empty(): msg = self.msg_queue.get() self.handle_message(msg) self.root.after(100, self.process_queue) def handle_message(self, msg): mtype = msg.get('type') if mtype == 'DISCONNECT': self.log("Desconectado del servidor.") self.btn_connect.config(state='normal') self.btn_start.config(state='disabled') return if mtype == 'NEW_ROUND': r = msg.get('round') size = msg.get('grid_size') self.lbl_round.config(text=f"Ronda: {r}") self.log(f"--- NUEVA RONDA {r} ---") self.game_phase = 'PLACING' self.build_grid(size) self.btn_done.config(state='disabled') elif mtype == 'TURN_NOTIFY': self.log(msg.get('msg', 'Es tu turno')) elif mtype == 'BOMB_flash': x, y = msg.get('x'), msg.get('y') who = msg.get('who') self.log(f"Bomba puesta por {who}") btn = self.buttons.get((x,y)) if btn: orig = btn.cget('bg') btn.config(bg='orange', text='💣') self.root.after(1000, lambda b=btn: b.config(bg=orig, text='')) elif mtype == 'PHASE_PLAY': self.game_phase = 'PLAYING' self.log(msg.get('msg')) self.btn_done.config(state='normal') elif mtype == 'EXPLOSION': x, y = msg.get('x'), msg.get('y') lives = msg.get('lives') who = msg.get('who') self.log(f"EXPLOSIÓN de {who}!") self.lbl_lives.config(text=f"Vidas: {lives}") btn = self.buttons.get((x,y)) if btn: btn.config(bg='red', text='💥') elif mtype == 'SAFE': x, y = msg.get('x'), msg.get('y') btn = self.buttons.get((x,y)) if btn: btn.config(bg='lightgreen', relief='sunken') elif mtype == 'WARNING': self.log(f"[AVISO] {msg.get('msg')}") elif mtype == 'ROUND_WIN': self.log(f"GANADOR: {msg.get('msg')}") messagebox.showinfo("Ronda", msg.get('msg')) elif mtype == 'GAME_WIN': messagebox.showinfo("Victoria", "Juego Completado!") def build_grid(self, size): for child in self.game_frame.winfo_children(): child.destroy() self.buttons = {} for r in range(size): self.game_frame.rowconfigure(r, weight=1) self.game_frame.columnconfigure(r, weight=1) for c in range(size): btn = tk.Button(self.game_frame, bg='#dddddd') btn.grid(row=r, column=c, sticky='nsew', padx=1, pady=1) btn.config(command=lambda x=c, y=r: self.on_click(x,y)) self.buttons[(c,r)] = btn def on_click(self, x, y): if self.game_phase == 'PLACING': self.send({"type": "PLACE_BOMB", "x": x, "y": y}) elif self.game_phase == 'PLAYING': self.send({"type": "CLICK_CELL", "x": x, "y": y}) def start_game(self): self.send({"type": "START_GAME"}) def check_cleared(self): self.send({"type": "CHECK_DUNGEON_CLEARED"}) if __name__ == "__main__": root = tk.Tk() app = GameClient(root) root.mainloop()