From 01377eada9db78cf16facb9a1ecc8e242338929d Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:49:52 -0500 Subject: [PATCH] Updated Bots and map system --- client.py | 167 ++++++++------------- map.json | 172 ++++++++++++++++++++++ map.py | 119 +++++++++++++++ server.py | 433 ++++++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 712 insertions(+), 179 deletions(-) create mode 100644 map.json create mode 100644 map.py diff --git a/client.py b/client.py index a43c81a..68b03df 100644 --- a/client.py +++ b/client.py @@ -8,16 +8,12 @@ import math HOST = '127.0.0.1' PORT = 65432 - -# Racing physics constants. -ACCELERATION = 300.0 # pixels per second^2 -BRAKE_DECELERATION = 400.0 # pixels per second^2 -FRICTION = 200.0 # pixels per second^2 -TURN_SPEED = math.radians(120) # radians per second (e.g., 120° per second) -MAX_SPEED = 400.0 # maximum forward speed -MIN_SPEED = -200.0 # maximum reverse speed - -# Dimensions for the car. +ACCELERATION = 300.0 +BRAKE_DECELERATION = 400.0 +FRICTION = 200.0 +TURN_SPEED = math.radians(120) +MAX_SPEED = 400.0 +MIN_SPEED = -200.0 CAR_WIDTH = 40 CAR_HEIGHT = 20 @@ -48,23 +44,25 @@ def recv_event(sock): data = recvall(sock, msg_len) return pickle.loads(data) -# Global state. -players = {} # client_id -> {"username", "x", "y", "angle", "speed"} -my_client_id = None # Our unique client id from the server. -# Predicted state for our own car. +players = {} # client_id -> state from server +my_client_id = None predicted_state = { - "x": 100, "y": 100, - "angle": 0, # Facing right initially. + "x": 100, + "y": 100, + "angle": 0, "speed": 0, "keys": set(), - "username": "Guest" + "username": "Guest", + "checkpoint_index": 0, + "finished": False } -ping = 0 # measured ping in seconds -server_details = {} # Updated from server_info and state_update events. -map_obstacles = [] # Map obstacles from server info. +server_details = {} +map_data = {} # Will hold the map (roads, obstacles, checkpoints, finish lines) +ping = 0 +local_keys = set() def listen_to_server(sock): - global my_client_id, players, predicted_state, ping, server_details, map_obstacles + global my_client_id, players, predicted_state, ping, server_details, map_data while True: try: event = recv_event(sock) @@ -76,40 +74,21 @@ def listen_to_server(sock): print(f"[INFO] My client ID: {my_client_id}") elif event.name == "server_info": server_details = event.payload - map_obstacles = server_details.get("map", []) + map_data = 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": - # Expect players to have "x", "y", "angle", "speed", "username". players = event.payload.get("players", {}) - # 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) - server_angle = players[my_client_id].get("angle", 0) - server_speed = players[my_client_id].get("speed", 0) - 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 - predicted_state["angle"] = server_angle - predicted_state["speed"] = server_speed - else: - predicted_state["x"] += (server_x - predicted_state["x"]) * correction_factor - predicted_state["y"] += (server_y - predicted_state["y"]) * correction_factor - predicted_state["angle"] += (server_angle - predicted_state["angle"]) * correction_factor - predicted_state["speed"] += (server_speed - predicted_state["speed"]) * correction_factor + elif event.name == "race_reset": + predicted_state.update({ + "x": 100, + "y": 100, + "angle": 0, + "speed": 0, + "keys": set(), + "checkpoint_index": 0, + "finished": False + }) + print("[RACE] Race reset by server.") elif event.name == "pong": sent_time = event.payload.get("timestamp") if sent_time: @@ -120,26 +99,16 @@ def listen_to_server(sock): 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})) + send_event(sock, Event("ping", {"timestamp": ts})) time.sleep(1) def draw_car(surface, x, y, angle, color): - # Define a simple rectangle for the car. half_w = CAR_WIDTH / 2 half_h = CAR_HEIGHT / 2 - # Corners of the rectangle before rotation. - corners = [ - (-half_w, -half_h), - (half_w, -half_h), - (half_w, half_h), - (-half_w, half_h) - ] + corners = [(-half_w, -half_h), (half_w, -half_h), (half_w, half_h), (-half_w, half_h)] rotated = [] cos_a = math.cos(angle) sin_a = math.sin(angle) @@ -160,22 +129,19 @@ def main(): 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})) + for event_name in ["client_connect", "client_disconnect", "state_update", "race_reset"]: + send_event(sock, Event("register_event", {"event": event_name})) print(f"[INFO] Registered for event: {event_name}") - send_message(sock, Event("set_username", {"username": username})) + send_event(sock, Event("set_username", {"username": username})) pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Top Down Racing Multiplayer") clock = pygame.time.Clock() - local_keys = set() - while True: - dt = clock.tick(60) / 1000.0 # Delta time in seconds + dt = clock.tick(60) / 1000.0 for ev in pygame.event.get(): if ev.type == pygame.QUIT: @@ -194,7 +160,7 @@ def main(): 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})) + send_event(sock, Event("keydown", {"key": key_name})) elif ev.type == pygame.KEYUP: key_name = None if ev.key == pygame.K_LEFT: @@ -209,16 +175,13 @@ def main(): 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})) + send_event(sock, Event("keyup", {"key": key_name})) - # Racing physics update. - # Acceleration/Braking. if "up" in predicted_state["keys"]: predicted_state["speed"] += ACCELERATION * dt elif "down" in predicted_state["keys"]: predicted_state["speed"] -= BRAKE_DECELERATION * dt else: - # Apply friction to gradually slow the car. if predicted_state["speed"] > 0: predicted_state["speed"] -= FRICTION * dt if predicted_state["speed"] < 0: @@ -228,76 +191,74 @@ def main(): if predicted_state["speed"] > 0: predicted_state["speed"] = 0 - # Clamp speed. if predicted_state["speed"] > MAX_SPEED: predicted_state["speed"] = MAX_SPEED if predicted_state["speed"] < MIN_SPEED: predicted_state["speed"] = MIN_SPEED - # Steering. if "left" in predicted_state["keys"]: predicted_state["angle"] -= TURN_SPEED * dt if "right" in predicted_state["keys"]: predicted_state["angle"] += TURN_SPEED * dt - # Update position based on current speed and angle. predicted_state["x"] += predicted_state["speed"] * math.cos(predicted_state["angle"]) * dt predicted_state["y"] += predicted_state["speed"] * math.sin(predicted_state["angle"]) * dt - # Camera follows the local car. 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. + screen.fill((30, 30, 30)) + if server_details.get("map"): + mdata = server_details["map"] + if "road" in mdata: + for road in mdata["road"]: + rect = pygame.Rect(road["x"] - camera_offset[0], road["y"] - camera_offset[1], road["width"], road["height"]) + pygame.draw.rect(screen, (80,80,80), rect) + if "obstacle" in mdata: + for obs in mdata["obstacle"]: + rect = pygame.Rect(obs["x"] - camera_offset[0], obs["y"] - camera_offset[1], obs["width"], obs["height"]) + pygame.draw.rect(screen, (150,50,50), rect) + if "checkpoint" in mdata: + for cp in mdata["checkpoint"]: + rect = pygame.Rect(cp["x"] - camera_offset[0], cp["y"] - camera_offset[1], cp["width"], cp["height"]) + pygame.draw.rect(screen, (200,200,0), rect, 2) + if "finish_line" in mdata: + for fl in mdata["finish_line"]: + rect = pygame.Rect(fl["x"] - camera_offset[0], fl["y"] - camera_offset[1], fl["width"], fl["height"]) + pygame.draw.rect(screen, (0,0,255), rect, 2) - # Draw map obstacles (if any). - 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 players as cars. for cid, data in players.items(): - # Get car position and orientation. px = data.get("x", 100) py = data.get("y", 100) angle = data.get("angle", 0) uname = data.get("username", "Guest") - # For our own car, use the predicted state. if cid == my_client_id: px = predicted_state["x"] py = predicted_state["y"] angle = predicted_state["angle"] - color = (0, 0, 255) # Blue for local player. + color = (0, 0, 255) else: - color = (255, 0, 0) # Red for others. - draw_x = px - camera_offset[0] - draw_y = py - camera_offset[1] - draw_car(screen, draw_x, draw_y, angle, color) - # Draw username above the car. + color = (255, 0, 0) + draw_car(screen, px - camera_offset[0], py - camera_offset[1], angle, color) 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 - 30)) + screen.blit(name_surface, (px - camera_offset[0] - name_surface.get_width()//2, py - camera_offset[1] - 30)) - # Display ping and HUD info. ping_font = pygame.font.SysFont(None, 24) - ping_surface = ping_font.render(f"Ping: {int(ping * 1000)} ms", True, (255, 255, 0)) + ping_surface = ping_font.render(f"Ping: {int(ping*1000)} ms", True, (255, 255, 0)) screen.blit(ping_surface, (10, 10)) - 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"Speed: {int(predicted_state['speed'])} px/s | Users: {total_users}/{max_users} | Client ID: {my_client_id or 'N/A'} | Tickrate: {tickrate} tps", + 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__": diff --git a/map.json b/map.json new file mode 100644 index 0000000..f09f2a2 --- /dev/null +++ b/map.json @@ -0,0 +1,172 @@ +{ + "obstacle": [ + { + "x": -85, + "y": 4, + "width": 93, + "height": 856 + }, + { + "x": 4, + "y": 792, + "width": 1012, + "height": 21 + }, + { + "x": 982, + "y": -8, + "width": 48, + "height": 804 + }, + { + "x": -78, + "y": -82, + "width": 1068, + "height": 94 + }, + { + "x": 240, + "y": 12, + "width": 10, + "height": 687 + }, + { + "x": 427, + "y": 82, + "width": 9, + "height": 711 + }, + { + "x": 622, + "y": 10, + "width": 10, + "height": 709 + }, + { + "x": 791, + "y": 91, + "width": 20, + "height": 706 + } + ], + "road": [ + { + "x": 858, + "y": 75, + "width": 69, + "height": 585 + }, + { + "x": 655, + "y": 21, + "width": 266, + "height": 48 + }, + { + "x": 658, + "y": 45, + "width": 88, + "height": 734 + }, + { + "x": 465, + "y": 730, + "width": 281, + "height": 49 + }, + { + "x": 464, + "y": 47, + "width": 117, + "height": 717 + }, + { + "x": 271, + "y": 23, + "width": 309, + "height": 45 + }, + { + "x": 273, + "y": 56, + "width": 111, + "height": 727 + }, + { + "x": 38, + "y": 707, + "width": 346, + "height": 76 + }, + { + "x": 41, + "y": 71, + "width": 144, + "height": 705 + }, + { + "x": 858, + "y": 43, + "width": 64, + "height": 59 + } + ], + "checkpoint": [ + { + "x": 32, + "y": 371, + "width": 160, + "height": 10 + }, + { + "x": 263, + "y": 370, + "width": 130, + "height": 11 + }, + { + "x": 462, + "y": 17, + "width": 47, + "height": 53 + }, + { + "x": 462, + "y": 367, + "width": 120, + "height": 18 + }, + { + "x": 617, + "y": 727, + "width": 21, + "height": 53 + }, + { + "x": 666, + "y": 355, + "width": 88, + "height": 11 + }, + { + "x": 796, + "y": 21, + "width": 6, + "height": 51 + }, + { + "x": 855, + "y": 363, + "width": 75, + "height": 15 + } + ], + "finish_line": [ + { + "x": 844, + "y": 667, + "width": 108, + "height": 106 + } + ] +} \ No newline at end of file diff --git a/map.py b/map.py new file mode 100644 index 0000000..bb0fcd9 --- /dev/null +++ b/map.py @@ -0,0 +1,119 @@ +import pygame +import json +import sys + +# Editor settings. +SCREEN_WIDTH, SCREEN_HEIGHT = 1000, 800 +BACKGROUND_COLOR = (30, 30, 30) +FPS = 60 + +# Colors for different element types. +COLORS = { + "obstacle": (150, 50, 50), + "road": (100, 100, 100), + "checkpoint": (200, 200, 0), + "finish_line": (0, 0, 255) +} + +# Element types. +ELEMENT_TYPES = ["obstacle", "road", "checkpoint", "finish_line"] + +def main(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("Advanced Map Editor: Roads, Checkpoints & Finish Line") + clock = pygame.time.Clock() + font = pygame.font.SysFont(None, 24) + + # A list to store drawn elements as tuples: (type, rect dict) + elements = [] + current_type = "obstacle" # Default drawing type. + + drawing = False + start_pos = (0, 0) + current_rect = None + + instructions = [ + "Press O: Obstacle, R: Road, C: Checkpoint, F: Finish Line", + "Click & drag to draw. Press S to save, U to undo last element, ESC to exit." + ] + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + pygame.quit() + sys.exit() + elif event.key == pygame.K_o: + current_type = "obstacle" + elif event.key == pygame.K_r: + current_type = "road" + elif event.key == pygame.K_c: + current_type = "checkpoint" + elif event.key == pygame.K_f: + current_type = "finish_line" + elif event.key == pygame.K_s: + # Organize elements by type. + map_data = {etype: [] for etype in ELEMENT_TYPES} + for etype, rect in elements: + map_data[etype].append(rect) + with open('map.json', 'w') as f: + json.dump(map_data, f, indent=4) + print("Map saved to map.json") + elif event.key == pygame.K_u: + if elements: + removed = elements.pop() + print(f"Removed last element of type {removed[0]}") + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left click starts drawing. + drawing = True + start_pos = event.pos + elif event.type == pygame.MOUSEBUTTONUP: + if event.button == 1 and drawing: + drawing = False + end_pos = event.pos + x = min(start_pos[0], end_pos[0]) + y = min(start_pos[1], end_pos[1]) + width = abs(end_pos[0] - start_pos[0]) + height = abs(end_pos[1] - start_pos[1]) + if width > 5 and height > 5: + rect_dict = {"x": x, "y": y, "width": width, "height": height} + elements.append((current_type, rect_dict)) + current_rect = None + elif event.type == pygame.MOUSEMOTION: + if drawing: + end_pos = event.pos + x = min(start_pos[0], end_pos[0]) + y = min(start_pos[1], end_pos[1]) + width = abs(end_pos[0] - start_pos[0]) + height = abs(end_pos[1] - start_pos[1]) + current_rect = pygame.Rect(x, y, width, height) + + screen.fill(BACKGROUND_COLOR) + + # Draw saved elements. + for etype, rect_dict in elements: + rect = pygame.Rect(rect_dict["x"], rect_dict["y"], rect_dict["width"], rect_dict["height"]) + pygame.draw.rect(screen, COLORS.get(etype, (255,255,255)), rect) + + # Draw current preview rectangle. + if current_rect: + pygame.draw.rect(screen, COLORS.get(current_type, (255,255,255)), current_rect, 2) + + # Display instructions. + y_offset = 10 + for line in instructions: + surf = font.render(line, True, (255,255,255)) + screen.blit(surf, (10, y_offset)) + y_offset += 24 + type_text = font.render(f"Current Element Type: {current_type.upper()}", True, (255,255,255)) + screen.blit(type_text, (10, y_offset)) + + pygame.display.flip() + clock.tick(FPS) + +if __name__ == "__main__": + main() diff --git a/server.py b/server.py index 361ba07..fb990f4 100644 --- a/server.py +++ b/server.py @@ -4,30 +4,50 @@ import pickle import time import uuid import math +import json +import pygame # For the dashboard +# --- Configuration --- HOST = '127.0.0.1' PORT = 65432 -TICKRATE = 128 # ticks per second (sub-tick simulation) +TICKRATE = 128 # ticks per second MAX_USERS = 100 # Racing physics constants. -ACCELERATION = 300.0 # pixels per second^2 (forward) -BRAKE_DECELERATION = 400.0 # pixels per second^2 (when braking) -FRICTION = 200.0 # pixels per second^2 (natural deceleration) -TURN_SPEED = math.radians(120) # radians per second (steering) -MAX_SPEED = 400.0 # maximum forward speed (pixels/sec) -MIN_SPEED = -200.0 # maximum reverse speed (pixels/sec) +ACCELERATION = 300.0 # pixels per second^2 +BRAKE_DECELERATION = 400.0 # pixels per second^2 +FRICTION = 200.0 # pixels per second^2 +TURN_SPEED = math.radians(240) # radians per second +MAX_SPEED = 400.0 # pixels per second +MIN_SPEED = -200.0 # pixels per second -# Player appearance (for collision). -PLAYER_RADIUS = 20 +# Collision / rendering radius. +PLAYER_RADIUS = 10 -# Define map obstacles. -map_obstacles = [ - {"x": 200, "y": 150, "width": 100, "height": 300}, - {"x": 500, "y": 100, "width": 50, "height": 400}, - {"x": 100, "y": 500, "width": 600, "height": 50}, -] +# --- Sensor Configuration for Bots --- +BOT_SENSOR_RAY_COUNT = 90 +BOT_SENSOR_FOV = math.radians(180) # Field of view of 90 degrees +SENSOR_MAX_DISTANCE = 150 # Maximum sensor range +# --- Load Map from JSON --- +try: + with open('map.json', 'r') as f: + map_data = json.load(f) + print("[INFO] Loaded map.json successfully.") +except Exception as e: + print(f"[ERROR] Could not load map.json: {e}") + map_data = { + "obstacle": [], + "road": [], + "checkpoint": [], + "finish_line": [] + } +obstacles = map_data.get("obstacle", []) +roads = map_data.get("road", []) +checkpoints = map_data.get("checkpoint", []) +finish_lines = map_data.get("finish_line", []) + +# --- Collision Helpers --- def circle_rect_collision(cx, cy, radius, rect): closest_x = max(rect["x"], min(cx, rect["x"] + rect["width"])) closest_y = max(rect["y"], min(cy, rect["y"] + rect["height"])) @@ -35,6 +55,23 @@ def circle_rect_collision(cx, cy, radius, rect): dy = cy - closest_y return (dx * dx + dy * dy) < (radius * radius) +def rect_collision(cx, cy, rect): + return (rect["x"] <= cx <= rect["x"] + rect["width"] and + rect["y"] <= cy <= rect["y"] + rect["height"]) + +# --- Ray Casting for Sensors --- +def ray_cast(x, y, angle, max_distance): + step = 5 + for d in range(0, int(max_distance), step): + test_x = x + d * math.cos(angle) + test_y = y + d * math.sin(angle) + for obs in obstacles: + if (obs["x"] <= test_x <= obs["x"] + obs["width"] and + obs["y"] <= test_y <= obs["y"] + obs["height"]): + return d + return max_distance + +# --- Network Event Class & Helpers --- class Event: def __init__(self, name, payload): self.name = name @@ -43,16 +80,22 @@ class Event: def recvall(sock, n): data = b'' while len(data) < n: - packet = sock.recv(n - len(data)) + try: + packet = sock.recv(n - len(data)) + except Exception as e: + return None 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) + try: + data = pickle.dumps(event) + length = len(data) + sock.sendall(length.to_bytes(4, byteorder='big') + data) + except Exception as e: + print(f"[ERROR] send_event: {e}") def recv_event(sock): raw_len = recvall(sock, 4) @@ -60,15 +103,189 @@ def recv_event(sock): return None msg_len = int.from_bytes(raw_len, byteorder='big') data = recvall(sock, msg_len) + if data is None: + return None return pickle.loads(data) -# Global dictionaries. +# --- Global State --- event_subscriptions = {} # event name -> list of client sockets client_ids = {} # conn -> client_id -# Each client state now stores position, angle, speed, and keys. -client_states = {} # conn -> {"client_id", "username", "x", "y", "angle", "speed", "keys"} +client_states = {} # conn -> state dict (for human players) +bot_states = {} # bot_id -> state dict (with "bot": True) lock = threading.Lock() +reset_in_progress = False # Global flag to avoid repeated resets +# --- Bot AI (Smart with Configurable Sensors) --- +def update_bot(state, dt): + # Determine target based on checkpoints or finish line. + if checkpoints and state["checkpoint_index"] < len(checkpoints): + cp = checkpoints[state["checkpoint_index"]] + target_x = cp["x"] + cp["width"] / 2 + target_y = cp["y"] + cp["height"] / 2 + elif finish_lines: + fl = finish_lines[0] # assume one finish line + target_x = fl["x"] + fl["width"] / 2 + target_y = fl["y"] + fl["height"] / 2 + else: + target_x = state["x"] + math.cos(state["angle"]) * 100 + target_y = state["y"] + math.sin(state["angle"]) * 100 + + # Incentivize staying on roads. + def is_on_road(x, y): + for road in roads: + if rect_collision(x, y, road): + return True + return False + if not is_on_road(state["x"], state["y"]) and roads: + best_distance = float('inf') + best_center = None + for road in roads: + center_x = road["x"] + road["width"] / 2 + center_y = road["y"] + road["height"] / 2 + d = math.hypot(center_x - state["x"], center_y - state["y"]) + if d < best_distance: + best_distance = d + best_center = (center_x, center_y) + if best_center is not None: + # Blend target: 70% checkpoint/finish and 30% road center. + target_x = 0.7 * target_x + 0.3 * best_center[0] + target_y = 0.7 * target_y + 0.3 * best_center[1] + + state["target"] = (target_x, target_y) # For dashboard drawing + + # Compute desired angle toward target. + desired_angle_target = math.atan2(target_y - state["y"], target_x - state["x"]) + + # --- Sensor Readings --- + sensor_readings = [] + for i in range(BOT_SENSOR_RAY_COUNT): + if BOT_SENSOR_RAY_COUNT > 1: + offset = -BOT_SENSOR_FOV/2 + i * (BOT_SENSOR_FOV/(BOT_SENSOR_RAY_COUNT - 1)) + else: + offset = 0 + sensor_angle = state["angle"] + offset + distance = ray_cast(state["x"], state["y"], sensor_angle, SENSOR_MAX_DISTANCE) + sensor_readings.append((offset, distance)) + state["sensors"] = sensor_readings + + # --- Compute Obstacle Avoidance Vector --- + avoidance_x = 0 + avoidance_y = 0 + threshold = SENSOR_MAX_DISTANCE * 0.6 # Activate avoidance if obstacle is closer than 60% of max. + for offset, distance in sensor_readings: + if distance < threshold: + repulsion = (threshold - distance) / threshold + avoid_angle = state["angle"] + offset + avoidance_x -= repulsion * math.cos(avoid_angle) + avoidance_y -= repulsion * math.sin(avoid_angle) + avoidance_angle = 0 + avoidance_weight = 0 + if avoidance_x != 0 or avoidance_y != 0: + avoidance_angle = math.atan2(avoidance_y, avoidance_x) + avoidance_weight = math.hypot(avoidance_x, avoidance_y) + + # --- Combine Target and Avoidance --- + w_target = 1.0 + w_avoid = avoidance_weight + target_vec = (math.cos(desired_angle_target), math.sin(desired_angle_target)) + avoid_vec = (math.cos(avoidance_angle), math.sin(avoidance_angle)) if w_avoid > 0 else (0, 0) + combined_x = w_target * target_vec[0] + w_avoid * avoid_vec[0] + combined_y = w_target * target_vec[1] + w_avoid * avoid_vec[1] + desired_angle = math.atan2(combined_y, combined_x) + + # --- Determine Steering Keys --- + def normalize_angle(a): + while a > math.pi: + a -= 2*math.pi + while a < -math.pi: + a += 2*math.pi + return a + angle_diff = normalize_angle(desired_angle - state["angle"]) + desired_keys = {"up"} # Always accelerate. + if angle_diff > 0.1: + desired_keys.add("right") + elif angle_diff < -0.1: + desired_keys.add("left") + state["keys"] = desired_keys + +# --- Physics Update for All Entities --- +def update_physics(state, dt): + if 'up' in state['keys']: + state['speed'] += ACCELERATION * dt + elif 'down' in state['keys']: + state['speed'] -= BRAKE_DECELERATION * dt + else: + if state['speed'] > 0: + state['speed'] -= FRICTION * dt + if state['speed'] < 0: + state['speed'] = 0 + elif state['speed'] < 0: + state['speed'] += FRICTION * dt + if state['speed'] > 0: + state['speed'] = 0 + + if state['speed'] > MAX_SPEED: + state['speed'] = MAX_SPEED + if state['speed'] < MIN_SPEED: + state['speed'] = MIN_SPEED + + if 'left' in state['keys']: + state['angle'] -= TURN_SPEED * dt + if 'right' in state['keys']: + state['angle'] += TURN_SPEED * dt + + new_x = state['x'] + state['speed'] * math.cos(state['angle']) * dt + new_y = state['y'] + state['speed'] * math.sin(state['angle']) * dt + + collision = False + for obs in obstacles: + if circle_rect_collision(new_x, new_y, PLAYER_RADIUS, obs): + collision = True + break + if not collision: + state['x'] = new_x + state['y'] = new_y + else: + state['speed'] = 0 + + # --- Checkpoints and Finish Line --- + if "checkpoint_index" not in state: + state["checkpoint_index"] = 0 + if "finished" not in state: + state["finished"] = False + + if checkpoints and not state["finished"]: + idx = state["checkpoint_index"] + if idx < len(checkpoints): + cp = checkpoints[idx] + if rect_collision(state["x"], state["y"], cp): + state["checkpoint_index"] += 1 + print(f"[RACE] {state['username']} passed checkpoint {idx+1}") + for fl in finish_lines: + if rect_collision(state["x"], state["y"], fl) and not state["finished"]: + if (checkpoints and state["checkpoint_index"] >= len(checkpoints)) or not checkpoints: + state["finished"] = True + print(f"[RACE] {state['username']} has finished the race!") + +# --- Global Reset Function --- +def reset_race(): + with lock: + for state in list(client_states.values()) + list(bot_states.values()): + state["x"] = 100 + state["y"] = 100 + state["angle"] = 0 + state["speed"] = 0 + state["keys"] = set() + state["checkpoint_index"] = 0 + state["finished"] = False + print("[RACE] Race finished. Resetting all players.") + +def clear_reset_flag(): + global reset_in_progress + with lock: + reset_in_progress = False + +# --- Network Client Handler --- def handle_client(conn, addr): client_id = str(uuid.uuid4()) with lock: @@ -78,9 +295,11 @@ def handle_client(conn, addr): "username": "Guest", "x": 100, "y": 100, - "angle": 0, # Facing right initially. + "angle": 0, "speed": 0, - "keys": set() + "keys": set(), + "finished": False, + "checkpoint_index": 0 } print(f"[NEW CONNECTION] {addr} connected as {client_id}") @@ -88,12 +307,12 @@ def handle_client(conn, addr): send_event(conn, Event("self_id", {"client_id": client_id})) server_info = { "tickrate": TICKRATE, - "total_users": len(client_states), + "total_users": len(client_states) + len(bot_states), "max_users": MAX_USERS, "server_ip": HOST, "server_port": PORT, - "server_name": "My Top Down Racing Server", - "map": map_obstacles + "server_name": "Advanced Top Down Racing Server", + "map": map_data } send_event(conn, Event("server_info", server_info)) except Exception as e: @@ -132,7 +351,6 @@ def handle_client(conn, addr): elif event.name == "ping": timestamp = event.payload.get('timestamp') send_event(conn, Event("pong", {"timestamp": timestamp})) - # Any other events (e.g., mouse_move, shoot) are ignored for racing. except Exception as e: print(f"[ERROR] Exception with {client_ids.get(conn, 'unknown')}: {e}") finally: @@ -157,59 +375,41 @@ def broadcast_event(event_name, payload): print(f"[ERROR] Failed to send to client, removing: {e}") receivers.remove(client) +# --- Bot Spawning --- +def spawn_bot(): + bot_id = str(uuid.uuid4()) + bot_state = { + "client_id": bot_id, + "username": "Bot_" + bot_id[:4], + "x": 150, + "y": 150, + "angle": 0, + "speed": 0, + "keys": set(), + "finished": False, + "checkpoint_index": 0, + "bot": True + } + with lock: + bot_states[bot_id] = bot_state + print(f"[BOT] Spawned bot {bot_state['username']}") + +# --- Game Loop --- def game_loop(): + global reset_in_progress dt = 1 / TICKRATE while True: time.sleep(dt) with lock: - # Update each player's state using racing physics. - for state in client_states.values(): - # Acceleration or braking. - if 'up' in state['keys']: - state['speed'] += ACCELERATION * dt - elif 'down' in state['keys']: - state['speed'] -= BRAKE_DECELERATION * dt - else: - # Apply friction when no acceleration is active. - if state['speed'] > 0: - state['speed'] -= FRICTION * dt - if state['speed'] < 0: - state['speed'] = 0 - elif state['speed'] < 0: - state['speed'] += FRICTION * dt - if state['speed'] > 0: - state['speed'] = 0 + all_states = list(client_states.values()) + list(bot_states.values()) + for state in all_states: + if state.get("bot", False): + update_bot(state, dt) + update_physics(state, dt) + + - # Clamp speed. - if state['speed'] > MAX_SPEED: - state['speed'] = MAX_SPEED - if state['speed'] < MIN_SPEED: - state['speed'] = MIN_SPEED - # Steering. - if 'left' in state['keys']: - state['angle'] -= TURN_SPEED * dt - if 'right' in state['keys']: - state['angle'] += TURN_SPEED * dt - - # Compute new position. - new_x = state['x'] + state['speed'] * math.cos(state['angle']) * dt - new_y = state['y'] + state['speed'] * math.sin(state['angle']) * dt - - # Check collision with obstacles. - collision = False - for obs in map_obstacles: - if circle_rect_collision(new_x, new_y, PLAYER_RADIUS, obs): - collision = True - break - if not collision: - state['x'] = new_x - state['y'] = new_y - else: - # On collision, stop the car. - state['speed'] = 0 - - # Build payload to send to all clients. players_payload = {} for state in client_states.values(): players_payload[state['client_id']] = { @@ -217,9 +417,21 @@ def game_loop(): "x": state['x'], "y": state['y'], "angle": state['angle'], - "speed": state['speed'] + "speed": state['speed'], + "finished": state.get("finished", False) } - total_users = len(client_states) + for state in bot_states.values(): + players_payload[state['client_id']] = { + "username": state['username'], + "x": state['x'], + "y": state['y'], + "angle": state['angle'], + "speed": state['speed'], + "finished": state.get("finished", False), + "sensors": state.get("sensors", []), + "target": state.get("target", None) + } + total_users = len(client_states) + len(bot_states) payload = { "players": players_payload, "total_users": total_users, @@ -227,9 +439,78 @@ def game_loop(): } broadcast_event("state_update", payload) +# --- Server Dashboard (Visualization) --- +def server_dashboard(): + pygame.init() + DASH_WIDTH, DASH_HEIGHT = 1200, 900 + screen = pygame.display.set_mode((DASH_WIDTH, DASH_HEIGHT)) + pygame.display.set_caption("Server Dashboard: Full Race Course View") + clock = pygame.time.Clock() + font = pygame.font.SysFont(None, 20) + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + return + + screen.fill((20, 20, 20)) + + # Draw Roads. + for road in roads: + rect = pygame.Rect(road["x"], road["y"], road["width"], road["height"]) + pygame.draw.rect(screen, (80, 80, 80), rect) + + # Draw Obstacles. + for obs in obstacles: + rect = pygame.Rect(obs["x"], obs["y"], obs["width"], obs["height"]) + pygame.draw.rect(screen, (150, 50, 50), rect) + + # Draw Checkpoints. + for cp in checkpoints: + rect = pygame.Rect(cp["x"], cp["y"], cp["width"], cp["height"]) + pygame.draw.rect(screen, (200, 200, 0), rect, 2) + + # Draw Finish Lines. + for fl in finish_lines: + rect = pygame.Rect(fl["x"], fl["y"], fl["width"], fl["height"]) + pygame.draw.rect(screen, (0, 0, 255), rect, 2) + + # Draw Players. + with lock: + all_states = list(client_states.values()) + list(bot_states.values()) + for state in all_states: + x = int(state["x"]) + y = int(state["y"]) + color = (0, 255, 0) if not state.get("bot", False) else (255, 165, 0) + pygame.draw.circle(screen, color, (x, y), PLAYER_RADIUS) + name = state["username"] + label = font.render(name, True, (255,255,255)) + screen.blit(label, (x - label.get_width() // 2, y - PLAYER_RADIUS - 15)) + + # For bots, draw sensor rays and target line. + if state.get("bot", False): + for offset, distance in state.get("sensors", []): + ray_angle = state["angle"] + offset + end_x = int(x + distance * math.cos(ray_angle)) + end_y = int(y + distance * math.sin(ray_angle)) + ray_color = (255, 0, 0) if distance < SENSOR_MAX_DISTANCE else (0, 255, 0) + pygame.draw.line(screen, ray_color, (x, y), (end_x, end_y), 2) + if "target" in state and state["target"]: + tx, ty = state["target"] + pygame.draw.line(screen, (255,255,255), (x, y), (int(tx), int(ty)), 2) + + info_text = font.render("Server Dashboard - Full Race Course View", True, (255,255,255)) + screen.blit(info_text, (10, 10)) + pygame.display.flip() + clock.tick(30) + +# --- Server Startup --- def start_server(): print("[STARTING] Server is starting...") threading.Thread(target=game_loop, daemon=True).start() + threading.Thread(target=server_dashboard, daemon=True).start() + spawn_bot() # Spawn one bot (add more if desired) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, PORT)) s.listen()