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("", 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(' 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('