Spaces:
Running
Running
import pygame | |
import math | |
import numpy as np | |
class MapGenerator: | |
def __init__(self, width, height, difficulty, goals): | |
self.surface = pygame.Surface((width, height)) | |
self.width = width | |
self.height = height | |
self.difficulty = difficulty | |
self.goals = [] | |
self.num_goals = goals if isinstance(goals, int) else len(goals) if goals else 12 | |
self.curve_pts = [] # Added to store curve points | |
def update(self): | |
self.surface.fill((255, 255, 255)) | |
if self.difficulty == 1: | |
border = 50 | |
num_ctrl = 4 | |
x_coords = [i * (self.width/(num_ctrl-1)) for i in range(num_ctrl)] | |
x_coords[0], x_coords[-1] = border*2, self.width-border*2 | |
y_coords = np.random.randint(border*4, self.height-border*4, num_ctrl) | |
ctrl_pts = list(zip(x_coords, y_coords)) | |
curve_pts = self.compute_catmull_rom(ctrl_pts, 50) | |
road_width = 100 | |
if self.difficulty == 2: | |
border = 50 | |
num_ctrl = 5 | |
x_coords = [i * (self.width/(num_ctrl-1)) for i in range(num_ctrl)] | |
x_coords[0], x_coords[-1] = border*2, self.width-border*2 | |
# Generate y coordinates with alternating high/low pattern | |
y_coords = [] | |
# Generate first point randomly with safe bounds | |
min_y = int(border*5.5) | |
max_y = self.height - int(border*5.5) | |
# Ensure we have valid bounds | |
if min_y >= max_y: | |
min_y = border * 2 | |
max_y = self.height - border * 2 | |
if min_y >= max_y: # Still invalid, use fallback | |
min_y = 50 | |
max_y = self.height - 50 | |
first_y = np.random.randint(min_y, max_y) | |
y_coords.append(first_y) | |
# Determine if first point is high or low | |
is_first_high = first_y > self.height // 2 | |
# Generate remaining points alternating between high and low | |
for i in range(1, num_ctrl): | |
index_prev = i-1 | |
y_previous = y_coords[index_prev] | |
diff_min = 75 | |
diff_max = 225 | |
if is_first_high: | |
if (i - 1) % 2 == 0: # Previous was high (first point) | |
low_bound = max(border, y_previous - diff_max) | |
high_bound = min(self.height - border, y_previous - diff_min) | |
else: # Previous was low | |
low_bound = max(border, y_previous + diff_min) | |
high_bound = min(self.height - border, y_previous + diff_max) | |
else: | |
if (i - 1) % 2 == 0: # Previous was low (first point) | |
low_bound = max(border, y_previous + diff_min) | |
high_bound = min(self.height - border, y_previous + diff_max) | |
else: # Previous was high | |
low_bound = max(border, y_previous - diff_max) | |
high_bound = min(self.height - border, y_previous - diff_min) | |
# Ensure valid bounds | |
if low_bound >= high_bound: | |
# Fallback: use a safe range around the previous point | |
center = y_previous | |
safe_range = 50 | |
low_bound = max(border, center - safe_range) | |
high_bound = min(self.height - border, center + safe_range) | |
if low_bound >= high_bound: | |
# Ultimate fallback | |
low_bound = border | |
high_bound = self.height - border | |
y = np.random.randint(low_bound, high_bound) | |
y_coords.append(y) | |
ctrl_pts = list(zip(x_coords, y_coords)) | |
curve_pts = self.compute_catmull_rom(ctrl_pts, 25) | |
curve_pts = self.compute_catmull_rom(curve_pts, 50) | |
road_width = 100 | |
self.curve_pts = curve_pts # Store curve points | |
# Create goals | |
goals_pts = self.create_goals(curve_pts) | |
self.goals = goals_pts | |
# Draw roads as connected segments | |
self.draw_road_segments(curve_pts, road_width) | |
# Draw goals | |
for goal_index in goals_pts: | |
pygame.draw.line(self.surface, (0,0,255), goal_index[0], goal_index[1], width=2) | |
# Get car start | |
if len(curve_pts) >= 2: | |
start_pos = curve_pts[0] | |
xs, ys = start_pos | |
xe, ye = curve_pts[1] | |
x = xe - xs | |
y = ye - ys | |
start_dir = (x, y) | |
else: | |
start_pos = (100, self.height//2) | |
start_dir = (1, 0) | |
return pygame.math.Vector2(start_pos), pygame.math.Vector2(start_dir) | |
def draw_road_segments(self, points, road_width): | |
if len(points) < 2: | |
return | |
for i in range(len(points) - 1): | |
start_pos = (int(points[i][0]), int(points[i][1])) | |
end_pos = (int(points[i+1][0]), int(points[i+1][1])) | |
pygame.draw.line(self.surface, (0, 0, 0), start_pos, end_pos, width=road_width) | |
cap_radius = road_width // 2 | |
for point in points: | |
pygame.draw.circle(self.surface, (0, 0, 0), (int(point[0]), int(point[1])), cap_radius) | |
def compute_catmull_rom(self, control_points, steps_per_segment=20): | |
try: | |
is_loop = control_points[0] == control_points[-1] | |
if hasattr(is_loop, '__len__') and len(is_loop) > 1: | |
is_loop = np.allclose(control_points[0], control_points[-1]) | |
except: | |
first = np.array(control_points[0]) | |
last = np.array(control_points[-1]) | |
is_loop = np.allclose(first, last) | |
pts = control_points[:-1] if is_loop else control_points | |
n = len(pts) | |
points = [] | |
def get(idx): | |
if is_loop: | |
return pts[idx % n] | |
else: | |
return pts[min(max(idx, 0), n - 1)] | |
for i in range(n if is_loop else n - 1): | |
p0 = get(i - 1) | |
p1 = get(i) | |
p2 = get(i + 1) | |
p3 = get(i + 2) | |
for j in range(steps_per_segment): | |
t = j / steps_per_segment | |
t2 = t * t | |
t3 = t2 * t | |
f1 = -0.5 * t3 + t2 - 0.5 * t | |
f2 = 1.5 * t3 - 2.5 * t2 + 1.0 | |
f3 = -1.5 * t3 + 2.0 * t2 + 0.5 * t | |
f4 = 0.5 * t3 - 0.5 * t2 | |
x = f1 * p0[0] + f2 * p1[0] + f3 * p2[0] + f4 * p3[0] | |
y = f1 * p0[1] + f2 * p1[1] + f3 * p2[1] + f4 * p3[1] | |
points.append((x, y)) | |
if not is_loop: | |
points.append(tuple(pts[-1])) | |
return points | |
def resample_by_distance(self, points, desired_spacing): | |
if len(points) < 2: | |
return points | |
dists = [0.0] | |
for i in range(1, len(points)): | |
prev, curr = np.array(points[i-1]), np.array(points[i]) | |
dist = float(np.linalg.norm(curr - prev)) | |
dists.append(dists[-1] + dist) | |
total_len = dists[-1] | |
if total_len == 0: | |
return points | |
n_points = int(total_len // desired_spacing) | |
new_pts = [] | |
for k in range(n_points+1): | |
target = k * desired_spacing | |
for j in range(1, len(dists)): | |
if dists[j] >= target: | |
ratio = (target - dists[j-1])/(dists[j] - dists[j-1]) | |
A, B = np.array(points[j-1]), np.array(points[j]) | |
P = A + ratio*(B - A) | |
new_pts.append(tuple(P)) | |
break | |
return new_pts if new_pts else points | |
def create_goals(self, pts, goal_number=None): | |
n_pts = len(pts) | |
if goal_number is not None: | |
num_goals = goal_number | |
else: | |
num_goals = self.num_goals | |
if n_pts == 0 or num_goals == 0: | |
return [] | |
actual_goals = min(num_goals, n_pts // 10) | |
if actual_goals == 0: | |
actual_goals = 1 | |
lines = [] | |
last_dir = None | |
for k in range(actual_goals): | |
if actual_goals == 1: | |
idx = n_pts // 2 | |
else: | |
idx = int((k * (n_pts - 1)) / (actual_goals - 1)) | |
idx = max(0, min(idx, n_pts - 1)) | |
idx2 = idx | |
look_ahead = 5 | |
if idx + look_ahead < n_pts: | |
idx2 = idx + look_ahead | |
elif idx >= look_ahead: | |
idx2 = idx - look_ahead | |
elif idx + 1 < n_pts: | |
idx2 = idx + 1 | |
elif idx > 0: | |
idx2 = idx - 1 | |
x, y = pts[idx] | |
if idx2 != idx: | |
x1, y1 = pts[idx2] | |
dir_vec = pygame.math.Vector2(x1 - x, y1 - y) | |
if idx2 < idx: | |
dir_vec *= -1 | |
else: | |
dir_vec = last_dir if last_dir else pygame.math.Vector2(0, -1) | |
if dir_vec.length_squared() < 1e-8: | |
dir_vec = pygame.math.Vector2(0, -1) | |
else: | |
dir_vec = dir_vec.normalize() | |
if last_dir and dir_vec.dot(last_dir) < -0.5: | |
dir_vec *= -1 | |
last_dir = dir_vec | |
normal = dir_vec.rotate(90) | |
p0 = pygame.math.Vector2(x, y) + normal * 50 | |
p1 = pygame.math.Vector2(x, y) - normal * 50 | |
lines.append((p0, p1)) | |
return lines | |
def draw(self, surface): | |
surface.blit(self.surface, (0,0)) | |
class Car: | |
def __init__(self, image, pos, max_velocity=275, turn_speed=77.5): | |
self.original_image = image.convert_alpha() | |
self.image = self.original_image | |
self.pos = pygame.math.Vector2(pos) | |
self.velocity = 0.0 | |
self.max_velocity = max_velocity | |
self.direction = pygame.math.Vector2(1, 0) | |
self.turn_speed = turn_speed | |
self.angle = 0.0 | |
self.is_braking = False | |
self.acceleration = 55.0 | |
self.deceleration = 16.0 | |
self.brake_deceleration = 55.0 | |
self.min_velocity_threshold = 5.0 | |
def update(self, dt, keys=None, steer_factor=1.0): | |
if keys is None: | |
pressed = pygame.key.get_pressed() | |
keys = { | |
'w': pressed[pygame.K_w], | |
'a': pressed[pygame.K_a], | |
's': pressed[pygame.K_s], | |
'd': pressed[pygame.K_d] | |
} | |
self.is_braking = False | |
if keys.get('w', False): | |
acceleration_multiplier = 1.0 | |
if self.velocity > 50: | |
acceleration_multiplier = 1.2 | |
elif self.velocity < 20: | |
acceleration_multiplier = 1.5 | |
if self.velocity < self.max_velocity: | |
self.velocity += self.acceleration * acceleration_multiplier * dt | |
if self.velocity > self.max_velocity: | |
self.velocity = self.max_velocity | |
if keys.get('s', False): | |
self.is_braking = True | |
if self.velocity > 0: | |
brake_multiplier = 1.0 + (self.velocity / self.max_velocity) * 0.5 | |
self.velocity -= self.brake_deceleration * brake_multiplier * dt | |
if self.velocity < 0: | |
self.velocity = 0 | |
else: | |
if not keys.get('w', False) and self.velocity > 0: | |
decel_rate = self.deceleration | |
if self.velocity > 100: | |
decel_rate *= 0.7 | |
self.velocity -= decel_rate * dt | |
if self.velocity < self.min_velocity_threshold: | |
self.velocity = max(0, self.velocity - 2.0 * dt) | |
if self.velocity > 0: | |
self.pos += self.direction.normalize() * self.velocity * dt | |
if self.velocity > 10: | |
angular_velocity = 0 | |
speed_factor = min(1.0, self.velocity / 200.0) | |
effective_turn_speed = self.turn_speed * (0.5 + 0.5 * speed_factor) | |
if keys.get('a', False): | |
angular_velocity = -effective_turn_speed * steer_factor | |
elif keys.get('d', False): | |
angular_velocity = effective_turn_speed * steer_factor | |
if angular_velocity != 0: | |
angle_change = angular_velocity * dt | |
target_direction = self.direction.rotate(angle_change) | |
base_drift = 0.15 | |
velocity_factor = min(1.0, self.velocity / self.max_velocity) | |
drift_factor = base_drift * (0.5 + 0.5 * velocity_factor) | |
self.direction = (self.direction * drift_factor + | |
target_direction * (1 - drift_factor)).normalize() | |
self.angle = -math.degrees(math.atan2(self.direction.y, self.direction.x)) | |
self.image = pygame.transform.rotate(self.original_image, self.angle) | |
def draw(self, surface): | |
rect = self.image.get_rect(center=self.pos) | |
surface.blit(self.image, rect.topleft) | |
def reset(self, pos, direction): | |
self.pos = pygame.math.Vector2(pos) | |
self.direction = pygame.math.Vector2(direction).normalize() | |
self.velocity = 0.0 | |
self.angle = -math.degrees(math.atan2(self.direction.y, self.direction.x)) | |
class RayCasting: | |
def __init__(self, car_pos, car_dir, track_surf, | |
ray_angles, ray_offsets, max_distance=1600, step_pixels=1, draw_rays=True): | |
self.car_pos = pygame.math.Vector2(car_pos) | |
self.car_dir = pygame.math.Vector2(car_dir).normalize() | |
self.track_surf = track_surf | |
self.ray_angles = list(ray_angles) | |
self.ray_offsets = list(ray_offsets) | |
self.max_distance = max_distance | |
self.step_pixels = step_pixels | |
self.last_collisions = [self.car_pos.copy()] * len(self.ray_angles) | |
self.ray_distances = [0.0] * len(self.ray_angles) | |
self.draw_rays = draw_rays | |
# NEW: Performance optimization variables | |
self.coarse_step = 15 # Check every x pixels first | |
self.fine_step = 1 # Then refine with 1 pixel precision | |
def cast_ray(self, start_pos, direction): | |
"""Optimized ray casting with coarse-to-fine approach""" | |
pos = pygame.math.Vector2(start_pos) | |
dir_norm = pygame.math.Vector2(direction).normalize() | |
# Calculate step size based on direction | |
inv = max(abs(dir_norm.x), abs(dir_norm.y)) | |
base_step_size = (1.0 / inv) if inv != 0 else 1.0 | |
# Phase 1: Coarse search with 5x step size | |
coarse_step_size = base_step_size * self.coarse_step | |
traveled = 0.0 | |
w, h = self.track_surf.get_size() | |
last_safe_pos = pos.copy() | |
last_safe_distance = 0.0 | |
while traveled < self.max_distance: | |
ix, iy = int(pos.x), int(pos.y) | |
if 0 <= ix < w and 0 <= iy < h: | |
color = self.track_surf.get_at((ix, iy)) | |
if color[:3] == (255, 255, 255): | |
# Collision found, break for fine search | |
break | |
# Store last safe position | |
last_safe_pos = pos.copy() | |
last_safe_distance = traveled | |
pos += dir_norm * coarse_step_size | |
traveled += coarse_step_size | |
# Phase 2: Fine search from last safe position | |
if traveled < self.max_distance: | |
# Reset to last safe position and do fine search | |
pos = last_safe_pos.copy() | |
traveled = last_safe_distance | |
fine_step_size = base_step_size * self.fine_step | |
while traveled < self.max_distance: | |
ix, iy = int(pos.x), int(pos.y) | |
if 0 <= ix < w and 0 <= iy < h: | |
color = self.track_surf.get_at((ix, iy)) | |
if color[:3] == (255, 255, 255): | |
return pos, traveled | |
pos += dir_norm * fine_step_size | |
traveled += fine_step_size | |
return pos, traveled | |
def draw_ray(self, start, end, color=(255, 255, 0)): | |
pygame.draw.line(self.track_surf, color, | |
(int(start.x), int(start.y)), | |
(int(end.x), int(end.y)), | |
width=2) | |
def update(self, car_pos, car_dir): | |
"""Updated to use optimized ray casting""" | |
self.car_pos = pygame.math.Vector2(car_pos) | |
self.car_dir = pygame.math.Vector2(car_dir).normalize() | |
self.last_collisions = [] | |
self.ray_distances = [] | |
any_origin_in_white = False | |
for angle, offset in zip(self.ray_angles, self.ray_offsets): | |
ray_dir = self.car_dir.rotate(angle).normalize() | |
origin = self.car_pos + ray_dir * offset | |
ix, iy = int(origin.x), int(origin.y) | |
w, h = self.track_surf.get_size() | |
if 0 <= ix < w and 0 <= iy < h: | |
if self.track_surf.get_at((ix, iy))[:3] == (255, 255, 255): | |
any_origin_in_white = True | |
self.last_collisions.append(origin) | |
self.ray_distances.append(0.0) | |
continue | |
# Use optimized ray casting | |
hit_pt, distance = self.cast_ray(origin, ray_dir) | |
self.last_collisions.append(hit_pt) | |
self.ray_distances.append(distance) | |
if self.draw_rays: | |
self.draw_ray(origin, hit_pt) | |
return not any_origin_in_white | |
def get_ray_distances(self): | |
return self.ray_distances.copy() | |
def has_zero_length_ray(self): | |
return any(distance <= 0.0 for distance in self.ray_distances) |