Updated Bots and map system

This commit is contained in:
OusmBlueNinja 2025-03-31 11:49:52 -05:00
parent 9b6defebe8
commit 01377eada9
4 changed files with 712 additions and 179 deletions

167
client.py
View File

@ -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__":

172
map.json Normal file
View File

@ -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
}
]
}

119
map.py Normal file
View File

@ -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()

433
server.py
View File

@ -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()