2025-04-08 16:28:40 +00:00
|
|
|
|
#!/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)
|
2025-04-09 00:58:12 +00:00
|
|
|
|
THROTTLE_RPM_RATE = 8000 # When throttle is pressed, engine spins up
|
2025-04-08 16:28:40 +00:00
|
|
|
|
BRAKE_RPM_RATE = 2500 # When braking, engine RPM drops rapidly
|
2025-04-09 00:58:12 +00:00
|
|
|
|
DAMPING_FACTOR = 1 # How strongly engine RPM is forced toward the "ideal" RPM
|
2025-04-08 16:28:40 +00:00
|
|
|
|
|
|
|
|
|
# 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.
|
2025-04-09 00:58:12 +00:00
|
|
|
|
ACCELERATION_CONVERSION = 10 # (rpm difference) to mph/s conversion factor
|
2025-04-08 16:28:40 +00:00
|
|
|
|
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.
|
2025-04-09 00:58:12 +00:00
|
|
|
|
K = 0.1
|
2025-04-08 16:28:40 +00:00
|
|
|
|
|
|
|
|
|
# Maximum vehicle speed (in mph)
|
2025-04-09 00:58:12 +00:00
|
|
|
|
MAX_SPEED = 9000000
|
2025-04-08 16:28:40 +00:00
|
|
|
|
|
|
|
|
|
# ----- 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])
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
def closest_point_on_segment(P, A, B):
|
|
|
|
|
"""Return the closest point on line segment AB to point P."""
|
|
|
|
|
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 A
|
|
|
|
|
t = max(0, min(1, (AP[0]*AB[0] + AP[1]*AB[1]) / ab2))
|
|
|
|
|
return (A[0] + t * AB[0], A[1] + t * AB[1])
|
|
|
|
|
|
2025-04-08 16:28:40 +00:00
|
|
|
|
# ----- 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_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 ---
|
|
|
|
|
accel = (self.engine_rpm - ideal_rpm) / ACCELERATION_CONVERSION
|
|
|
|
|
accel -= DRAG_COEFF * self.speed
|
|
|
|
|
self.speed += accel * dt
|
|
|
|
|
if self.speed < 0:
|
|
|
|
|
self.speed = 0
|
|
|
|
|
if self.speed > MAX_SPEED:
|
|
|
|
|
self.speed = MAX_SPEED
|
|
|
|
|
|
|
|
|
|
# --- Update Position ---
|
|
|
|
|
direction = 1 if self.gear != 0 else -1
|
2025-04-09 00:58:12 +00:00
|
|
|
|
ft_per_sec = self.speed * 1.46667 # Convert mph to ft/s
|
2025-04-08 16:28:40 +00:00
|
|
|
|
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)."""
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
def draw_map_view(surface, track, car):
|
|
|
|
|
"""
|
|
|
|
|
Draw a mini-map in the bottom-right corner displaying all generated road segments
|
|
|
|
|
and the car's current position.
|
|
|
|
|
"""
|
|
|
|
|
map_width = 200
|
|
|
|
|
map_height = 150
|
|
|
|
|
map_x = SCREEN_WIDTH - map_width - 10
|
|
|
|
|
map_y = SCREEN_HEIGHT - map_height - 10
|
|
|
|
|
map_rect = pygame.Rect(map_x, map_y, map_width, map_height)
|
|
|
|
|
|
|
|
|
|
# Draw mini-map background and border
|
|
|
|
|
pygame.draw.rect(surface, (30, 30, 30), map_rect)
|
|
|
|
|
pygame.draw.rect(surface, (255, 255, 255), map_rect, 2)
|
|
|
|
|
|
|
|
|
|
# Compute bounding box for all track segments
|
|
|
|
|
if track.segments:
|
|
|
|
|
xs = []
|
|
|
|
|
ys = []
|
|
|
|
|
for seg in track.segments:
|
|
|
|
|
A, B = seg
|
|
|
|
|
xs.extend([A[0], B[0]])
|
|
|
|
|
ys.extend([A[1], B[1]])
|
|
|
|
|
min_x, max_x = min(xs), max(xs)
|
|
|
|
|
min_y, max_y = min(ys), max(ys)
|
|
|
|
|
else:
|
|
|
|
|
min_x, max_x, min_y, max_y = 0, 1, 0, 1
|
|
|
|
|
|
|
|
|
|
# Add margins
|
|
|
|
|
margin = 10
|
|
|
|
|
min_x -= margin
|
|
|
|
|
min_y -= margin
|
|
|
|
|
max_x += margin
|
|
|
|
|
max_y += margin
|
|
|
|
|
|
|
|
|
|
world_width = max_x - min_x
|
|
|
|
|
world_height = max_y - min_y
|
|
|
|
|
if world_width == 0 or world_height == 0:
|
|
|
|
|
scale = 1
|
|
|
|
|
else:
|
|
|
|
|
scale = min((map_width - 20) / world_width, (map_height - 20) / world_height)
|
|
|
|
|
|
|
|
|
|
# Convert world coordinates to mini-map coordinates
|
|
|
|
|
def world_to_map(pos):
|
|
|
|
|
world_x, world_y = pos
|
|
|
|
|
map_pos_x = map_x + 10 + (world_x - min_x) * scale
|
|
|
|
|
map_pos_y = map_y + 10 + (world_y - min_y) * scale
|
|
|
|
|
return (int(map_pos_x), int(map_pos_y))
|
|
|
|
|
|
|
|
|
|
# Draw road segments on mini-map
|
|
|
|
|
for seg in track.segments:
|
|
|
|
|
A, B = seg
|
|
|
|
|
map_A = world_to_map(A)
|
|
|
|
|
map_B = world_to_map(B)
|
|
|
|
|
pygame.draw.line(surface, (200, 200, 200), map_A, map_B, 2)
|
|
|
|
|
|
|
|
|
|
# Draw the car's current position as a small circle
|
|
|
|
|
car_map_pos = world_to_map((car.x, car.y))
|
|
|
|
|
pygame.draw.circle(surface, (255, 0, 0), car_map_pos, 4)
|
|
|
|
|
|
2025-04-08 16:28:40 +00:00
|
|
|
|
# ----- 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:
|
|
|
|
|
if event.key == pygame.K_e and car.gear < 10:
|
|
|
|
|
car.gear += 1
|
|
|
|
|
car.rev_match_on_shift_up()
|
|
|
|
|
elif event.key == pygame.K_q and car.gear > 1:
|
|
|
|
|
car.gear -= 1
|
|
|
|
|
car.rev_match_on_shift_down()
|
|
|
|
|
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)
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
# Determine view offset to center the car.
|
2025-04-08 16:28:40 +00:00
|
|
|
|
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))
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
# If off road, draw blue line to nearest point on road.
|
|
|
|
|
if not is_on_road(car, track, ROAD_WIDTH):
|
|
|
|
|
P = (car.x, car.y)
|
|
|
|
|
min_distance = float('inf')
|
|
|
|
|
closest_point = None
|
|
|
|
|
for seg in track.segments:
|
|
|
|
|
A, B = seg
|
|
|
|
|
candidate = closest_point_on_segment(P, A, B)
|
|
|
|
|
d = math.hypot(P[0] - candidate[0], P[1] - candidate[1])
|
|
|
|
|
if d < min_distance:
|
|
|
|
|
min_distance = d
|
|
|
|
|
closest_point = candidate
|
|
|
|
|
if closest_point is not None:
|
|
|
|
|
start_screen = (int(car.x + offset_x), int(car.y + offset_y))
|
|
|
|
|
end_screen = (int(closest_point[0] + offset_x), int(closest_point[1] + offset_y))
|
|
|
|
|
pygame.draw.line(screen, (0, 0, 255), start_screen, end_screen, 2)
|
|
|
|
|
|
2025-04-08 16:28:40 +00:00
|
|
|
|
# Draw gauges.
|
|
|
|
|
draw_gauge(screen, SPEED_GAUGE_CENTER, GAUGE_RADIUS, 0, MAX_SPEED, car.speed, "Speed (mph)")
|
|
|
|
|
draw_gauge(screen, TACH_GAUGE_CENTER, GAUGE_RADIUS, 0, REDLINE_RPM, car.engine_rpm, "RPM")
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
# Display gear information.
|
2025-04-08 16:28:40 +00:00
|
|
|
|
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))
|
|
|
|
|
|
2025-04-09 00:58:12 +00:00
|
|
|
|
# ----- Draw Map View -----
|
|
|
|
|
draw_map_view(screen, track, car)
|
|
|
|
|
|
2025-04-08 16:28:40 +00:00
|
|
|
|
pygame.display.flip()
|
|
|
|
|
|
|
|
|
|
pygame.quit()
|
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|