sword_game/client/main.py

265 lines
9.6 KiB
Python

import socket
import threading
import struct
import customtkinter as ctk
from enum import IntEnum
import Protocol_pb2 as Protocol
class PacketID(IntEnum):
Ping = 1
CS_Login = 10
SC_LoginResult = 11
Chat = 20
CS_UpgradeSword = 30
SC_UpgradeResult = 31
CS_SellSword = 35
SC_SellResult = 36
CS_RankingRequest = 40
SC_RankingList = 41
class RankingType(IntEnum):
LEVEL = 0
GOLD = 1
class SwordGameClient(ctk.CTk):
def __init__(self):
super().__init__()
self.title("C++ Sword Game - Python Client")
self.geometry("1200x800")
ctk.set_appearance_mode("dark")
# 네트워크 관련
self.socket = None
self.connected = False
self.nickname = ""
# 게임 정보
self.current_level = 0
self.current_gold = 0
# UI 초기화
self.setup_login_ui()
def setup_login_ui(self):
self.login_frame = ctk.CTkFrame(self)
self.login_frame.pack(expand=True)
self.label = ctk.CTkLabel(self.login_frame, text="검 키우기 온라인", font=("Arial", 24, "bold"))
self.label.pack(pady=20)
self.nickname_entry = ctk.CTkEntry(self.login_frame, placeholder_text="닉네임 입력")
self.nickname_entry.pack(pady=10)
self.login_button = ctk.CTkButton(self.login_frame, text="접속", command=self.connect_to_server)
self.login_button.pack(pady=20)
def setup_game_ui(self):
for widget in self.winfo_children():
widget.destroy()
# 메인 레이아웃
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=2)
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
# 왼쪽: 유저 정보 창
self.info_frame = ctk.CTkFrame(self)
self.info_frame.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
self.user_label = ctk.CTkLabel(self.info_frame, text=f"플레이어: {self.nickname}", font=("Arial", 16))
self.user_label.pack(pady=10)
self.gold_label = ctk.CTkLabel(self.info_frame, text="골드: 0", font=("Arial", 18, "bold"), text_color="#FFD700")
self.gold_label.pack(pady=5)
self.level_label = ctk.CTkLabel(self.info_frame, text="검 레벨: 0", font=("Arial", 20, "bold"))
self.level_label.pack(pady=10)
self.upgrade_btn = ctk.CTkButton(self.info_frame, text="강화 시도", fg_color="#28a745", hover_color="#218838", command=self.send_upgrade)
self.upgrade_btn.pack(pady=10, fill="x", padx=20)
self.sell_btn = ctk.CTkButton(self.info_frame, text="판매 하기", fg_color="#dc3545", hover_color="#c82333", command=self.send_sell)
self.sell_btn.pack(pady=10, fill="x", padx=20)
self.ranking_btn = ctk.CTkButton(self.info_frame, text="랭킹 갱신", fg_color="#17a2b8", hover_color="#138496", command=self.request_rankings)
self.ranking_btn.pack(pady=10, fill="x", padx=20)
# 오른쪽: 채팅 창
self.chat_frame = ctk.CTkFrame(self)
self.chat_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
self.chat_box = ctk.CTkTextbox(self.chat_frame, state="disabled")
self.chat_box.pack(expand=True, fill="both", padx=10, pady=10)
self.chat_entry = ctk.CTkEntry(self.chat_frame, placeholder_text="메시지 입력...")
self.chat_entry.pack(fill="x", padx=10, pady=(0, 10))
self.chat_entry.bind("<Return>", lambda e: self.send_chat())
# 오른쪽: 랭킹 창
self.ranking_frame = ctk.CTkFrame(self)
self.ranking_frame.grid(row=0, column=2, padx=20, pady=20, sticky="nsew")
self.rank_title = ctk.CTkLabel(self.ranking_frame, text="실시간 랭킹", font=("Arial", 18, "bold"))
self.rank_title.pack(pady=10)
self.rank_tab = ctk.CTkSegmentedButton(self.ranking_frame, values=["강화 단계", "자산"], command=self.on_rank_tab_changed)
self.rank_tab.set("강화 단계")
self.rank_tab.pack(pady=5, padx=10, fill="x")
self.rank_list_box = ctk.CTkTextbox(self.ranking_frame, state="disabled", font=("Courier New", 12))
self.rank_list_box.pack(expand=True, fill="both", padx=10, pady=10)
self.my_rank_label = ctk.CTkLabel(self.ranking_frame, text="내 순위: -", font=("Arial", 14, "bold"), text_color="#FFD700")
self.my_rank_label.pack(pady=10)
self.request_rankings()
def connect_to_server(self):
self.nickname = self.nickname_entry.get()
if not self.nickname:
return
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect(("127.0.0.1", 30000))
self.connected = True
threading.Thread(target=self.receive_loop, daemon=True).start()
pkt = Protocol.CS_Login()
pkt.nickname = self.nickname
self.send_packet(PacketID.CS_Login, pkt)
except Exception as e:
print(f"Connection failed: {e}")
self.nickname_entry.configure(placeholder_text="연결 실패!")
def send_packet(self, packet_id, protobuf_obj=None):
if not self.socket: return
payload = b""
if protobuf_obj:
payload = protobuf_obj.SerializeToString()
# Header: size (2 bytes) + id (2 bytes), Little Endian
header = struct.pack('<HH', len(payload), int(packet_id))
self.socket.sendall(header + payload)
def receive_loop(self):
try:
while self.connected:
header_data = self.socket.recv(4)
if not header_data: break
size, pkt_id = struct.unpack('<HH', header_data)
payload = b""
if size > 0:
payload = self.socket.recv(size)
self.handle_packet(pkt_id, payload)
except Exception as e:
print(f"Receive error: {e}")
finally:
self.connected = False
def handle_packet(self, pkt_id, payload):
if pkt_id == PacketID.SC_LoginResult:
res = Protocol.SC_LoginResult()
res.ParseFromString(payload)
if res.success:
self.current_gold = res.gold
self.current_level = res.sword_level
self.after(0, self.setup_game_ui)
self.after(0, self.update_stats)
else:
print("Login Failed")
elif pkt_id == PacketID.SC_UpgradeResult:
res = Protocol.SC_UpgradeResult()
res.ParseFromString(payload)
self.current_level = res.current_level
self.current_gold = res.current_gold
self.after(0, self.update_stats)
elif pkt_id == PacketID.SC_SellResult:
res = Protocol.SC_SellResult()
res.ParseFromString(payload)
self.current_level = 0
self.current_gold = res.total_gold
self.after(0, self.update_stats)
self.after(0, lambda: self.log_chat(f"[시스템] 검 판매 완료! {res.earned_gold} 골드 획득."))
elif pkt_id == PacketID.Chat:
msg = payload.decode('utf-8')
self.after(0, lambda: self.log_chat(msg))
elif pkt_id == PacketID.SC_RankingList:
res = Protocol.SC_RankingList()
res.ParseFromString(payload)
self.after(0, lambda: self.update_ranking_ui(res))
def update_stats(self):
self.level_label.configure(text=f"검 레벨: {self.current_level}")
self.gold_label.configure(text=f"골드: {self.current_gold:,}")
def send_upgrade(self):
self.send_packet(PacketID.CS_UpgradeSword)
def send_sell(self):
self.send_packet(PacketID.CS_SellSword)
def send_chat(self):
msg = self.chat_entry.get()
if msg:
header = struct.pack('<HH', len(msg.encode('utf-8')), int(PacketID.Chat))
self.socket.sendall(header + msg.encode('utf-8'))
self.chat_entry.delete(0, 'end')
def log_chat(self, msg):
self.chat_box.configure(state="normal")
self.chat_box.insert("end", msg + "\n")
self.chat_box.see("end")
self.chat_box.configure(state="disabled")
def request_rankings(self):
tab = self.rank_tab.get()
rtype = RankingType.LEVEL if tab == "강화 단계" else RankingType.GOLD
pkt = Protocol.CS_RankingRequest()
pkt.type = rtype
self.send_packet(PacketID.CS_RankingRequest, pkt)
def on_rank_tab_changed(self, value):
self.request_rankings()
def update_ranking_ui(self, res):
self.rank_list_box.configure(state="normal")
self.rank_list_box.delete("1.0", "end")
header = f"{'순위':<4} {'닉네임':<12} {'':<10}\n"
self.rank_list_box.insert("end", header)
self.rank_list_box.insert("end", "-" * 30 + "\n")
for entry in res.entries:
val_str = f"{entry.value:,}"
if res.type == RankingType.LEVEL:
val_str = f"{entry.value}단계"
line = f"{entry.rank:<4} {entry.nickname:<12} {val_str:<10}\n"
self.rank_list_box.insert("end", line)
self.rank_list_box.configure(state="disabled")
if res.my_rank != -1:
my_val_str = f"{res.my_value:,}"
if res.type == RankingType.LEVEL:
my_val_str = f"{res.my_value}단계"
self.my_rank_label.configure(text=f"내 순위: {res.my_rank}위 ({my_val_str})")
else:
self.my_rank_label.configure(text="내 순위: 기록 없음")
if __name__ == "__main__":
app = SwordGameClient()
app.mainloop()