import socket import threading import pickle import pygame import sys import time import math HOST = '127.0.0.1' PORT = 65432 MOVE_SPEED = 200 # pixels per second GUN_LENGTH = 30 PLAYER_RADIUS = 20 BULLET_RADIUS = 4 FLASH_EFFECT_RADIUS = 200 FLASH_DURATION = 1.0 # must match server value class Event: def __init__(self, name, payload): self.name = name self.payload = payload def recvall(sock, n): data = b'' while len(data) < n: packet = sock.recv(n - len(data)) if not packet: return None data += packet return data def send_event(sock, event): data = pickle.dumps(event) length = len(data) sock.sendall(length.to_bytes(4, byteorder='big') + data) def recv_event(sock): raw_len = recvall(sock, 4) if not raw_len: return None msg_len = int.from_bytes(raw_len, byteorder='big') data = recvall(sock, msg_len) return pickle.loads(data) # Global state. players = {} # client_id -> {"username", "x", "y", "mouse_x", "mouse_y", "health"} bullets = [] # List of bullet dicts from server. grenades = [] # List of grenade dicts. my_client_id = None # Our unique client id from the server. predicted_state = { "x": 100, "y": 100, "keys": set(), "username": "Guest", "mouse_x": 0, "mouse_y": 0 } ping = 0 # measured ping in seconds server_details = {} # Updated from server_info and state_update events. map_obstacles = [] # Map obstacles from server info. def listen_to_server(sock): global my_client_id, players, predicted_state, ping, server_details, bullets, grenades, map_obstacles while True: try: event = recv_event(sock) if event is None: break if event.name == "self_id": if my_client_id is None: my_client_id = event.payload.get("client_id") print(f"[INFO] My client ID: {my_client_id}") elif event.name == "server_info": server_details = event.payload map_obstacles = server_details.get("map", []) print(f"[INFO] Received server info: {server_details}") elif event.name == "client_connect": print(f"[INFO] Client connected: {event.payload.get('client_id')}") elif event.name == "client_disconnect": cid = event.payload.get("client_id") print(f"[INFO] Client disconnected: {cid}") if cid in players: del players[cid] elif event.name == "state_update": players = event.payload.get("players", {}) bullets = event.payload.get("bullets", []) grenades = event.payload.get("grenades", []) # Update dynamic server details. if "total_users" in event.payload: server_details["total_users"] = event.payload["total_users"] if "max_users" in event.payload: server_details["max_users"] = event.payload["max_users"] if my_client_id and my_client_id in players: server_x = players[my_client_id].get("x", 100) server_y = players[my_client_id].get("y", 100) tickrate = server_details.get("tickrate", 64) correction_factor = 0.1 * (64 / tickrate) if not predicted_state["keys"]: predicted_state["x"] = server_x predicted_state["y"] = server_y else: predicted_state["x"] += (server_x - predicted_state["x"]) * correction_factor predicted_state["y"] += (server_y - predicted_state["y"]) * correction_factor elif event.name == "pong": sent_time = event.payload.get("timestamp") if sent_time: round_trip = time.time() - sent_time ping = round_trip print(f"[PING] {ping*1000:.0f} ms") except Exception as e: print(f"[ERROR] {e}") break def send_message(sock, event): send_event(sock, event) def ping_loop(sock): while True: ts = time.time() send_message(sock, Event("ping", {"timestamp": ts})) time.sleep(1) def main(): global my_client_id, predicted_state username = input("Enter your username: ") predicted_state["username"] = username sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) threading.Thread(target=listen_to_server, args=(sock,), daemon=True).start() threading.Thread(target=ping_loop, args=(sock,), daemon=True).start() # Register for events. for event_name in ["client_connect", "client_disconnect", "state_update"]: send_message(sock, Event("register_event", {"event": event_name})) print(f"[INFO] Registered for event: {event_name}") send_message(sock, Event("set_username", {"username": username})) pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Top Down Shooter Multiplayer") clock = pygame.time.Clock() local_keys = set() while True: dt = clock.tick(60) / 1000.0 for ev in pygame.event.get(): if ev.type == pygame.QUIT: pygame.quit() sys.exit() elif ev.type == pygame.KEYDOWN: key_name = None if ev.key == pygame.K_LEFT: key_name = "left" elif ev.key == pygame.K_RIGHT: key_name = "right" elif ev.key == pygame.K_UP: key_name = "up" elif ev.key == pygame.K_DOWN: key_name = "down" # Grenade throw keys. elif ev.key == pygame.K_q: send_message(sock, Event("throw_smoke", {})) print("[ACTION] Smoke grenade thrown") elif ev.key == pygame.K_e: send_message(sock, Event("throw_flash", {})) print("[ACTION] Flash grenade thrown") if key_name and key_name not in local_keys: local_keys.add(key_name) predicted_state["keys"].add(key_name) send_message(sock, Event("keydown", {"key": key_name})) elif ev.type == pygame.KEYUP: key_name = None if ev.key == pygame.K_LEFT: key_name = "left" elif ev.key == pygame.K_RIGHT: key_name = "right" elif ev.key == pygame.K_UP: key_name = "up" elif ev.key == pygame.K_DOWN: key_name = "down" if key_name and key_name in local_keys: local_keys.remove(key_name) if key_name in predicted_state["keys"]: predicted_state["keys"].remove(key_name) send_message(sock, Event("keyup", {"key": key_name})) elif ev.type == pygame.MOUSEMOTION: mouse_x, mouse_y = ev.pos predicted_state["mouse_x"] = mouse_x predicted_state["mouse_y"] = mouse_y send_message(sock, Event("mouse_move", {"x": mouse_x, "y": mouse_y})) elif ev.type == pygame.MOUSEBUTTONDOWN: if ev.button == 1: # Left click: shoot mouse_x, mouse_y = ev.pos dx = mouse_x - predicted_state["x"] dy = mouse_y - predicted_state["y"] angle = math.atan2(dy, dx) send_message(sock, Event("shoot", {"angle": angle})) print("[ACTION] Shoot event sent") # Update predicted state locally. dx = 0 dy = 0 if "left" in predicted_state["keys"]: dx -= MOVE_SPEED * dt if "right" in predicted_state["keys"]: dx += MOVE_SPEED * dt if "up" in predicted_state["keys"]: dy -= MOVE_SPEED * dt if "down" in predicted_state["keys"]: dy += MOVE_SPEED * dt predicted_state["x"] += dx predicted_state["y"] += dy # Camera follows local player. local_x = predicted_state["x"] local_y = predicted_state["y"] screen_width, screen_height = 800, 600 camera_offset = (local_x - screen_width // 2, local_y - screen_height // 2) # Drawing. screen.fill((30, 30, 30)) # Background. # Draw map obstacles. for obs in map_obstacles: rect = pygame.Rect(obs["x"] - camera_offset[0], obs["y"] - camera_offset[1], obs["width"], obs["height"]) pygame.draw.rect(screen, (100, 100, 100), rect) # Draw bullets. for bullet in bullets: bx = int(bullet.get("x", 0) - camera_offset[0]) by = int(bullet.get("y", 0) - camera_offset[1]) pygame.draw.circle(screen, (255, 255, 255), (bx, by), BULLET_RADIUS) # Draw grenades. for gren in grenades: gx = int(gren.get("x", 0) - camera_offset[0]) gy = int(gren.get("y", 0) - camera_offset[1]) gtype = gren.get("type", "") if gren.get("fuse", 0) > 0: pygame.draw.circle(screen, (200, 200, 200), (gx, gy), 6) else: if gtype == "smoke": # Draw semi-transparent smoke flood fill. smoke_radius = 80 smoke_surface = pygame.Surface((smoke_radius*2, smoke_radius*2), pygame.SRCALPHA) pygame.draw.circle(smoke_surface, (100, 100, 100, 150), (smoke_radius, smoke_radius), smoke_radius) screen.blit(smoke_surface, (gx - smoke_radius, gy - smoke_radius)) elif gtype == "flash": pygame.draw.circle(screen, (255, 255, 0), (gx, gy), 30) # Draw players. for cid, data in players.items(): # Get player position and health. px = data.get("x", 100) py = data.get("y", 100) health = data.get("health", 100) uname = data.get("username", "Guest") # Determine aiming direction. if cid == my_client_id: aim_dx = predicted_state["mouse_x"] - local_x aim_dy = predicted_state["mouse_y"] - local_y else: aim_dx = data.get("mouse_x", 0) - px aim_dy = data.get("mouse_y", 0) - py aim_angle = math.atan2(aim_dy, aim_dx) # Adjust drawing positions by camera offset. draw_x = int(px - camera_offset[0]) draw_y = int(py - camera_offset[1]) # Draw player body. if cid == my_client_id: color = (0, 0, 255) else: color = (255, 0, 0) pygame.draw.circle(screen, color, (draw_x, draw_y), PLAYER_RADIUS) # Draw face (an eye) indicating aim. eye_offset = 10 eye_x = draw_x + int(eye_offset * math.cos(aim_angle)) eye_y = draw_y + int(eye_offset * math.sin(aim_angle)) pygame.draw.circle(screen, (255, 255, 255), (eye_x, eye_y), 4) # Draw health bar above player. bar_width = 40 bar_height = 5 health_ratio = health / 100 health_bar_back = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, bar_width, bar_height) health_bar_front = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, int(bar_width * health_ratio), bar_height) pygame.draw.rect(screen, (100, 100, 100), health_bar_back) pygame.draw.rect(screen, (0, 255, 0), health_bar_front) # Draw username. font = pygame.font.SysFont(None, 20) name_surface = font.render(uname, True, (255, 255, 255)) screen.blit(name_surface, (draw_x - name_surface.get_width()//2, draw_y - PLAYER_RADIUS - 30)) # Flash effect: if a flash grenade is active and the local player is nearby, overlay white. flash_alpha = 0 for gren in grenades: if gren.get("type") == "flash" and gren.get("fuse", 0) <= 0: gx = gren.get("x", 0) gy = gren.get("y", 0) dist = math.hypot(local_x - gx, local_y - gy) if dist < FLASH_EFFECT_RADIUS: intensity = gren.get("duration", 0) / FLASH_DURATION flash_alpha = max(flash_alpha, int(255 * intensity)) if flash_alpha > 0: flash_overlay = pygame.Surface((screen_width, screen_height)) flash_overlay.fill((255, 255, 255)) flash_overlay.set_alpha(flash_alpha) screen.blit(flash_overlay, (0, 0)) # Display ping. ping_font = pygame.font.SysFont(None, 24) ping_surface = ping_font.render(f"Ping: {int(ping*1000)} ms", True, (255, 255, 0)) screen.blit(ping_surface, (10, 10)) # Display HUD info. hud_font = pygame.font.SysFont(None, 24) total_users = server_details.get("total_users", len(players)) max_users = server_details.get("max_users", "?") tickrate = server_details.get("tickrate", 64) hud_text = hud_font.render( f"Users: {total_users}/{max_users} | Client ID: {my_client_id or 'N/A'} | Tickrate: {tickrate} tps", True, (255, 255, 255) ) screen.blit(hud_text, (10, 40)) pygame.display.flip() if __name__ == "__main__": main()