#!/usr/bin/env python3 """ A single-player pygame racing simulation where engine RPM and speed (in mph) are modeled as independent—but coupled—systems. The engine’s RPM (tachometer) is updated by throttle/brake input plus a damping effect that tends to “lock” the engine to the value expected from the current vehicle speed and gear. Vehicle acceleration is computed from the RPM difference (engine load) minus drag, yielding a more realistic feel. Controls: - LEFT/RIGHT arrows: steer - UP arrow: throttle (increases engine RPM and, via the dynamics, accelerates the car) - DOWN arrow: brake - E: upshift (gear +1 with rev-match drop) - Q: downshift (gear -1 with a slight rev bump) - R: toggle reverse (gear 0) versus forward (gear 1) """ import sys import math import random import pygame # ----- Screen & Road Configuration ----- SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 ROAD_WIDTH = 150 # Road drawn as a wide filled polygon FPS = 60 # ----- Engine & Vehicle Dynamics ----- IDLE_RPM = 800 REDLINE_RPM = 6000 # Engine dynamics (in rpm/sec) THROTTLE_RPM_RATE = 800 # When throttle is pressed, engine spins up BRAKE_RPM_RATE = 2500 # When braking, engine RPM drops rapidly DAMPING_FACTOR = 5 # How strongly engine RPM is forced toward the "ideal" RPM # These dynamics model the connection between engine torque and vehicle acceleration: # The vehicle's acceleration (in mph/s) is proportional to the difference between the # current engine RPM and the ideal RPM that would be produced by the current speed in the given gear. ACCELERATION_CONVERSION = 150 # (rpm difference) to mph/s conversion factor DRAG_COEFF = 0.02 # Drag (per second) that always slows the vehicle # Conversion constant (empirical) for what engine RPM should be for a given vehicle speed # in a particular gear. In this model, ideal_rpm = speed × gear_ratio × K. K = 21 # Maximum vehicle speed (in mph) MAX_SPEED = 90 # ----- Transmission & Gear Ratios ----- # These gear ratios (including reverse = gear 0) are assumed to incorporate the effect # of the transmission (and final drive) so that: # ideal_rpm = speed (mph) × gear_ratio × K. GEAR_RATIOS = { 0: 4.866, # Reverse 1: 4.696, 2: 2.985, 3: 2.146, 4: 1.769, 5: 1.520, 6: 1.275, 7: 1.000, 8: 0.854, 9: 0.689, 10: 0.636 } # Turning rate (radians per second) TURN_RATE = 2.0 # ----- Gauge Configuration ----- GAUGE_RADIUS = 50 SPEED_GAUGE_CENTER = (150, 100) TACH_GAUGE_CENTER = (650, 100) # ----- Helper Functions ----- def rotate_point(point, angle): """Rotate a 2D point by angle in radians.""" x, y = point cos_a = math.cos(angle) sin_a = math.sin(angle) return (x * cos_a - y * sin_a, x * sin_a + y * cos_a) def draw_gauge(surface, center, radius, min_val, max_val, current_val, label): """ Draw an analog gauge (circle with a needle) mapping a value from min_val to max_val to an angle between -135° and +135°. """ pygame.draw.circle(surface, (255, 255, 255), center, radius, 2) min_angle = -135 max_angle = 135 ratio = (current_val - min_val) / (max_val - min_val) if max_val != min_val else 0 angle_deg = min_angle + ratio * (max_angle - min_angle) angle_rad = math.radians(angle_deg) needle_length = radius - 10 needle_end = (center[0] + needle_length * math.cos(angle_rad), center[1] + needle_length * math.sin(angle_rad)) pygame.draw.line(surface, (255, 0, 0), center, needle_end, 3) font = pygame.font.SysFont(None, 24) text_surf = font.render(f"{label}: {int(current_val)}", True, (255, 255, 255)) text_rect = text_surf.get_rect(center=(center[0], center[1] + radius + 20)) surface.blit(text_surf, text_rect) def point_line_distance(P, A, B): """Return the shortest distance from point P to line segment AB.""" AB = (B[0] - A[0], B[1] - A[1]) AP = (P[0] - A[0], P[1] - A[1]) ab2 = AB[0]**2 + AB[1]**2 if ab2 == 0: return math.hypot(P[0] - A[0], P[1] - A[1]) t = max(0, min(1, (AP[0]*AB[0] + AP[1]*AB[1]) / ab2)) proj = (A[0] + t*AB[0], A[1] + t*AB[1]) return math.hypot(P[0] - proj[0], P[1] - proj[1]) # ----- Game Objects ----- class Car: def __init__(self): self.x = 0.0 # Position (in feet or arbitrary units that scale to mph) self.y = 0.0 self.angle = 0.0 # Orientation (radians; 0 means facing right) self.engine_rpm = IDLE_RPM self.gear = 1 # 0 = reverse, 1..10 forward self.speed = 0.0 # Vehicle speed in mph def update(self, throttle, brake, dt): """ Update engine RPM and vehicle speed over time. - The engine RPM is updated based on throttle input, brake input, and a damping term that forces it toward the "ideal" RPM determined by the current speed. - The vehicle acceleration is then computed from the difference between actual and ideal RPM. """ # Compute the ideal engine RPM given the current speed and gear. # For realism, if the car is stopped, ideal RPM should be at least IDLE_RPM. ideal_rpm = max(IDLE_RPM, self.speed * GEAR_RATIOS[self.gear] * K) # --- Engine RPM Dynamics --- # Throttle increases engine RPM; braking drops it. throttle_effect = THROTTLE_RPM_RATE if throttle else 0 brake_effect = BRAKE_RPM_RATE if brake else 0 # Engine tends to return toward the ideal RPM. d_rpm = throttle_effect - brake_effect - DAMPING_FACTOR * (self.engine_rpm - ideal_rpm) self.engine_rpm += d_rpm * dt # Clamp engine RPM between IDLE and REDLINE. if self.engine_rpm < IDLE_RPM: self.engine_rpm = IDLE_RPM if self.engine_rpm > REDLINE_RPM: self.engine_rpm = REDLINE_RPM # --- Vehicle Acceleration Dynamics --- # The acceleration is proportional to how much the engine is "over-revving" relative to ideal. # A positive difference means extra torque; a negative difference means the engine is not producing enough torque. accel = (self.engine_rpm - ideal_rpm) / ACCELERATION_CONVERSION # Subtract drag proportional to speed. accel -= DRAG_COEFF * self.speed # Update vehicle speed. self.speed += accel * dt if self.speed < 0: self.speed = 0 if self.speed > MAX_SPEED: self.speed = MAX_SPEED # --- Update Position --- # When in reverse (gear 0), the vehicle moves backward. direction = 1 if self.gear != 0 else -1 # Convert speed (mph) to distance per second. (1 mph ≈ 1.46667 ft/s) ft_per_sec = self.speed * 1.46667 self.x += math.cos(self.angle) * ft_per_sec * dt * direction self.y += math.sin(self.angle) * ft_per_sec * dt * direction def rev_match_on_shift_up(self): """Simulate a realistic drop in RPM when upshifting (rev-match).""" # On an upshift, the engine RPM typically drops by 10–20%. self.engine_rpm *= 0.85 if self.engine_rpm < IDLE_RPM: self.engine_rpm = IDLE_RPM def rev_match_on_shift_down(self): """Simulate a realistic bump in RPM when downshifting.""" self.engine_rpm *= 1.15 if self.engine_rpm > REDLINE_RPM: self.engine_rpm = REDLINE_RPM class Track: """ A procedural track that extends as the car moves, giving the illusion of an infinite road. """ def __init__(self): self.segments = [] self.last_point = (0.0, 0.0) self.last_angle = 0.0 self.generate_initial_track() def generate_initial_track(self): num_segments = 20 for _ in range(num_segments): self.extend_segment() def extend_segment(self): length = 150 angle_change = random.uniform(-0.3, 0.3) self.last_angle += angle_change dx = length * math.cos(self.last_angle) dy = length * math.sin(self.last_angle) next_point = (self.last_point[0] + dx, self.last_point[1] + dy) self.segments.append((self.last_point, next_point)) self.last_point = next_point def ensure_track_length(self, car_pos, min_distance=500): dist = math.hypot(self.last_point[0] - car_pos[0], self.last_point[1] - car_pos[1]) while dist < min_distance: self.extend_segment() dist = math.hypot(self.last_point[0] - car_pos[0], self.last_point[1] - car_pos[1]) def is_on_road(car, track, road_width): """ Checks whether the car's center is within half the road width of any track segment. """ P = (car.x, car.y) hw = road_width / 2 for seg in track.segments: A, B = seg if point_line_distance(P, A, B) <= hw: return True return False # ----- Drawing Functions ----- def draw_road(surface, track, offset_x, offset_y, road_width): """ Draw each track segment as a filled polygon representing a wide road. """ for seg in track.segments: start, end = seg dx = end[0] - start[0] dy = end[1] - start[1] seg_len = math.hypot(dx, dy) if seg_len == 0: continue # Compute a perpendicular vector. nx = -dy / seg_len ny = dx / seg_len half_w = road_width / 2 start_left = (start[0] + nx * half_w, start[1] + ny * half_w) start_right = (start[0] - nx * half_w, start[1] - ny * half_w) end_left = (end[0] + nx * half_w, end[1] + ny * half_w) end_right = (end[0] - nx * half_w, end[1] - ny * half_w) poly = [ (int(start_left[0] + offset_x), int(start_left[1] + offset_y)), (int(end_left[0] + offset_x), int(end_left[1] + offset_y)), (int(end_right[0] + offset_x), int(end_right[1] + offset_y)), (int(start_right[0] + offset_x), int(start_right[1] + offset_y)) ] pygame.draw.polygon(surface, (100, 100, 100), poly) # Draw a center line for visual effect. center_start = (start[0] + offset_x, start[1] + offset_y) center_end = (end[0] + offset_x, end[1] + offset_y) pygame.draw.line(surface, (255, 255, 255), center_start, center_end, 2) def draw_car(surface, car, offset_x, offset_y): """ Draw the car as a rotated, car-like polygon. """ center = (car.x + offset_x, car.y + offset_y) half_length = 40 / 2 half_width = 20 / 2 # Define a 5-point polygon for the car shape. points = [ ( half_length, 0), # Front tip ( half_length * 0.2, -half_width), # Rear top (-half_length, -half_width), # Rear left (-half_length, half_width), # Rear right ( half_length * 0.2, half_width) # Rear bottom ] rotated = [] for p in points: rp = rotate_point(p, car.angle) rotated.append((rp[0] + center[0], rp[1] + center[1])) pygame.draw.polygon(surface, (255, 0, 0), rotated) pygame.draw.polygon(surface, (255, 255, 255), rotated, 2) # ----- Main Game Loop ----- def main(): pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Super Realistic Car Simulation") clock = pygame.time.Clock() font = pygame.font.SysFont(None, 24) car = Car() track = Track() running = True while running: dt = clock.tick(FPS) / 1000.0 # ----- Event Handling ----- for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: # Upshift (E) if event.key == pygame.K_e and car.gear < 10: old_ratio = GEAR_RATIOS[car.gear] car.gear += 1 # Rev-match on upshift: drop RPM to roughly maintain consistency. car.rev_match_on_shift_up() # Downshift (Q), but do not downshift below gear 1. elif event.key == pygame.K_q and car.gear > 1: old_ratio = GEAR_RATIOS[car.gear] car.gear -= 1 car.rev_match_on_shift_down() # Toggle reverse (R) elif event.key == pygame.K_r: if car.gear == 0: car.gear = 1 else: car.gear = 0 # ----- Continuous Input ----- keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: car.angle -= TURN_RATE * dt if keys[pygame.K_RIGHT]: car.angle += TURN_RATE * dt throttle = keys[pygame.K_UP] brake = keys[pygame.K_DOWN] # ----- Update Car & Extend Track ----- car.update(throttle, brake, dt) track.ensure_track_length((car.x, car.y), min_distance=500) # Determine the view offset so the car is centered. offset_x = SCREEN_WIDTH / 2 - car.x offset_y = SCREEN_HEIGHT / 2 - car.y # ----- Drawing ----- screen.fill((0, 150, 0)) # Grass background. draw_road(screen, track, offset_x, offset_y, ROAD_WIDTH) draw_car(screen, car, offset_x, offset_y) # Display status (on-road/off-road) status_text = "On Road" if is_on_road(car, track, ROAD_WIDTH) else "Off Road" status_surf = font.render(status_text, True, (255, 255, 255)) screen.blit(status_surf, (10, 10)) # Draw gauges. # Speed gauge shows vehicle speed in mph. draw_gauge(screen, SPEED_GAUGE_CENTER, GAUGE_RADIUS, 0, MAX_SPEED, car.speed, "Speed (mph)") # Tachometer shows engine RPM. draw_gauge(screen, TACH_GAUGE_CENTER, GAUGE_RADIUS, 0, REDLINE_RPM, car.engine_rpm, "RPM") # Display current gear. gear_text = "Reverse" if (car.gear == 0) else f"Gear: {car.gear}" gear_surf = font.render(gear_text, True, (255, 255, 255)) screen.blit(gear_surf, (SPEED_GAUGE_CENTER[0] - GAUGE_RADIUS, SPEED_GAUGE_CENTER[1] + GAUGE_RADIUS + 40)) pygame.display.flip() pygame.quit() sys.exit() if __name__ == "__main__": main()