220 lines
7.6 KiB
Python
220 lines
7.6 KiB
Python
#!/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()
|