Spaces:
Sleeping
Sleeping
Download train/car_game_classes.py from IncreasingLoss/RL_Car_Agent: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/spaces/IncreasingLoss/RL_Car_Agent/resolve/main/train/car_game_classes.py
- Command line
-
hf download hf://spaces/IncreasingLoss/RL_Car_Agent/train/car_game_classes.py
-
curl -L -o car_game_classes.py https://huggingface.co/spaces/IncreasingLoss/RL_Car_Agent/resolve/main/train/car_game_classes.py
19.7 kB
| import pygame | |
| import sys | |
| import math | |
| import json | |
| 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, 800) | |
| road_width = 100 | |
| if self.difficulty == 2: | |
| # Define constants | |
| border = 150 | |
| cx = self.width / 2 | |
| cy = self.height / 2 | |
| r = min(self.width / 2 - border, self.height / 2 - border) | |
| a = r | |
| b = r | |
| # Generate control points for open loop (exclude last few points to create gap) | |
| N = 25 | |
| gap_points = 3 # Remove last 3 control points to create visible gap | |
| angles = [2 * np.pi * i / N for i in range(N - gap_points)] | |
| control_pts = [] | |
| # Perlin-like noise parameters for soft, organic wobble | |
| octaves = 1 | |
| frequencies = [2.5] # Multiple octaves for perlin-like effect | |
| amplitudes = [16] # Decreasing amplitudes for each octave | |
| # Random phases for each octave | |
| phi_x = [[np.random.uniform(0, 2 * np.pi) for _ in range(octaves)]] | |
| phi_y = [[np.random.uniform(0, 2 * np.pi) for _ in range(octaves)]] | |
| # Generate noisy control points with perlin-like noise | |
| for i, angle in enumerate(angles): | |
| x = cx + a * np.cos(angle) | |
| y = cy + b * np.sin(angle) | |
| # Apply multi-octave noise (perlin-like) | |
| dx = 0 | |
| dy = 0 | |
| for j in range(octaves): | |
| # Use both angle and position index for variation | |
| noise_input = frequencies[j] * angle + phi_x[0][j] | |
| dx += amplitudes[j] * np.sin(noise_input) | |
| noise_input_y = frequencies[j] * angle + phi_y[0][j] | |
| dy += amplitudes[j] * np.cos(noise_input_y) # Use cos for y to decorrelate | |
| x += dx | |
| y += dy | |
| control_pts.append((x, y)) | |
| # Randomly invert the list of control points | |
| if np.random.rand() < 0.5: | |
| control_pts = control_pts[::-1] | |
| # Compute the curve points with an open loop | |
| curve_pts = self.compute_catmull_rom(control_pts, 800) | |
| # Remove additional points at the end to ensure visible gap | |
| if len(curve_pts) > 100: | |
| curve_pts = curve_pts[:-50] # Remove last 50 curve points for clear gap | |
| road_width = 100 | |
| if self.difficulty == 3: | |
| 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 = [] | |
| # Define high and low ranges | |
| high_range = (self.height//2 + 2*border, self.height - int(border*5.5)) | |
| low_range = (int(border*5.5), self.height//2 - 2*border) | |
| # Generate first point randomly | |
| first_y = np.random.randint(int(border*5.5), self.height-int(border*5.5)) | |
| 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): | |
| if is_first_high: | |
| # If previous point was high, make this one low | |
| if (i - 1) % 2 == 0: # Previous was high (first point) | |
| y = np.random.randint(low_range[0], low_range[1]) | |
| else: # Previous was low | |
| y = np.random.randint(high_range[0], high_range[1]) | |
| else: | |
| # If previous point was low, make this one high | |
| if (i - 1) % 2 == 0: # Previous was low (first point) | |
| y = np.random.randint(high_range[0], high_range[1]) | |
| else: # Previous was high | |
| y = np.random.randint(low_range[0], low_range[1]) | |
| y_coords.append(y) | |
| ctrl_pts = list(zip(x_coords, y_coords)) | |
| # 50% chance to flip the control points order | |
| if np.random.rand() < 0.5: | |
| ctrl_pts = ctrl_pts[::-1] | |
| curve_pts = self.compute_catmull_rom(ctrl_pts, 800) | |
| 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 | |
| def cast_ray(self, start_pos, direction): | |
| pos = pygame.math.Vector2(start_pos) | |
| dir_norm = pygame.math.Vector2(direction).normalize() | |
| inv = max(abs(dir_norm.x), abs(dir_norm.y)) | |
| step_size = (1.0 / inv) if inv != 0 else 1.0 | |
| traveled = 0.0 | |
| w, h = self.track_surf.get_size() | |
| 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 * (step_size * self.step_pixels) | |
| traveled += step_size * self.step_pixels | |
| 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): | |
| 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 | |
| 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) | |
| if __name__ == "__main__": | |
| pygame.init() | |
| screen = pygame.display.set_mode((1600, 900)) | |
| clock = pygame.time.Clock() | |
| car_surf = pygame.image.load("assets/car_transparent.png").convert_alpha() | |
| w, h = car_surf.get_size() | |
| car_surf = pygame.transform.scale(car_surf, (w//18, h//18)) | |
| car = Car(car_surf, pos=(400, 300)) | |
| map = MapGenerator(1600, 900, difficulty=3, goals=22) | |
| startpos, startdir = map.update() | |
| car.direction = startdir | |
| car.pos = startpos | |
| car.velocity = 0 | |
| ray_angles = [-60, -30, -15, 0, 15, 30, 60] | |
| ray_offsets = [12, 22, 27, 26, 27, 22, 12] | |
| rays = RayCasting(car.pos, car.direction, screen, ray_angles, ray_offsets) | |
| car.draw(screen) | |
| frames = 0 | |
| while True: | |
| frames += 1 | |
| dt = clock.tick(90) / 1000 | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: | |
| pygame.quit() | |
| sys.exit() | |
| rays.update(car.pos, car.direction) | |
| if frames % 3000 == 0 or rays.has_zero_length_ray(): | |
| dt = 0 | |
| startpos, startdir = map.update() | |
| car.direction = startdir | |
| car.pos = startpos | |
| car.velocity = 0 | |
| screen.fill((255, 255, 255)) | |
| map.draw(screen) | |
| car.update(dt) | |
| car.draw(screen) | |
| rays.update(car.pos, car.direction) | |
| pygame.display.flip() |