Car-Racing-Agent / game /tracks.py
nirmalpratheep's picture
Upload 7 files
de9fc8c verified
"""
tracks.py β€” Track definitions for the curriculum car racer.
Angle convention (pygame y-down):
0Β° = right (+x)
90Β° = down (+y)
180Β° = left (-x)
270Β° = up (-y)
"""
import math
import pygame
SCREEN_W, SCREEN_H = 900, 600
# Colours
C_GRASS = (45, 110, 45)
C_TRACK = (52, 52, 52)
C_WHITE = (255, 255, 255)
# ────────────────────────────────────────────────────────────────────────────
# Geometry helpers
# ────────────────────────────────────────────────────────────────────────────
def _arc(cx, cy, rx, ry, a0_deg, a1_deg, n=24):
"""Return n+1 points along an elliptical arc from a0_deg to a1_deg."""
pts = []
for i in range(n + 1):
t = a0_deg + (a1_deg - a0_deg) * i / n
rad = math.radians(t)
x = cx + rx * math.cos(rad)
y = cy + ry * math.sin(rad)
pts.append((x, y))
return pts
def _full_ellipse(cx, cy, rx, ry, n=80, start_deg=90):
"""Return n+1 points of a full ellipse starting at start_deg."""
return _arc(cx, cy, rx, ry, start_deg, start_deg + 360, n)
def _dense_poly(corners, step=20, segment_widths=None):
"""
Sample a closed straight-segment polygon at ~step-px intervals.
Analogous to _arc() for polygon tracks: produces dense waypoints so the
+10 lookahead in CarEnv._obs() gives meaningful corner anticipation.
If segment_widths (one value per corner segment) is provided, returns
(waypoints, expanded_widths) with widths broadcast to the dense point list.
Otherwise returns just the waypoints list.
"""
result = []
expanded_sw = [] if segment_widths is not None else None
n = len(corners)
for i in range(n):
x0, y0 = corners[i]
x1, y1 = corners[(i + 1) % n]
seg_len = math.hypot(x1 - x0, y1 - y0)
n_pts = max(2, int(seg_len / step))
for k in range(n_pts):
t = k / n_pts
result.append((x0 + t * (x1 - x0), y0 + t * (y1 - y0)))
if expanded_sw is not None:
expanded_sw.extend([segment_widths[i]] * n_pts)
if segment_widths is not None:
return result, expanded_sw
return result
def _ipts(pts):
"""Convert float point list to integer tuples."""
return [(int(round(x)), int(round(y))) for x, y in pts]
# ────────────────────────────────────────────────────────────────────────────
# TrackDef
# ────────────────────────────────────────────────────────────────────────────
class TrackDef:
def __init__(self, level, name, waypoints, width, start_pos, start_angle, max_speed,
segment_widths=None):
self.level = level
self.name = name
self.waypoints = waypoints # list of (x,y) floats
self.width = width
# Per-segment widths for variable-width tracks (one value per waypoint,
# applied to the segment FROM that waypoint TO the next).
# None = uniform self.width everywhere.
self.segment_widths = segment_widths
self.start_pos = start_pos # (x, y) floats
self.start_angle = start_angle # degrees
self.max_speed = max_speed
self.surface = None
self.mask = None
self.hud_corner = (8, 8) # default; updated after build()
# Unit vector in start_angle direction (for gate_side)
rad = math.radians(start_angle)
self._gate_dx = math.cos(rad)
self._gate_dy = math.sin(rad)
# ── Reward metadata (computed once here, used by CarEnv) ─────────────
# Perimeter of the waypoint polygon = approximate track centerline length
self.optimal_dist = sum(
math.hypot(waypoints[(i + 1) % len(waypoints)][0] - waypoints[i][0],
waypoints[(i + 1) % len(waypoints)][1] - waypoints[i][1])
for i in range(len(waypoints))
)
# Expected lap time (frames) at 70 % of max speed β€” accounts for corners
self.par_time_steps = self.optimal_dist / (max_speed * 0.70)
# Difficulty multiplier: narrow + fast = harder.
# For variable-width tracks the *choke* (minimum segment) sets difficulty.
_BASE_WIDTH = 115.0
_BASE_SPEED = 3.0
eff_w = min(segment_widths) if segment_widths else width
self.complexity = (_BASE_WIDTH / eff_w) * (max_speed / _BASE_SPEED)
# Road width at the start/finish line (for checkered flag rendering).
# For variable-width tracks, find the width of the segment nearest to start.
if segment_widths is not None:
sx, sy = start_pos
nearest = min(range(len(waypoints)),
key=lambda i: math.hypot(waypoints[i][0] - sx,
waypoints[i][1] - sy))
self._start_road_width = segment_widths[nearest]
else:
self._start_road_width = width
def _best_hud_corner(self, panel_w, panel_h, margin=8):
"""Return (x, y) of the screen corner with fewest track pixels under the HUD panel."""
corners = [
(margin, margin),
(SCREEN_W - panel_w - margin, margin),
(margin, SCREEN_H - panel_h - margin),
(SCREEN_W - panel_w - margin, SCREEN_H - panel_h - margin),
]
best_pos, best_count = corners[0], float('inf')
for cx, cy in corners:
count = sum(
1
for px in range(cx, cx + panel_w, 6)
for py in range(cy, cy + panel_h, 6)
if self.mask.get_at((px, py))[0] > 128
)
if count < best_count:
best_count, best_pos = count, (cx, cy)
return best_pos
def build(self):
"""Draw the track onto self.surface and build self.mask."""
BORDER = 6 # white border thickness on each edge (pixels)
surf = pygame.Surface((SCREEN_W, SCREEN_H))
surf.fill(C_GRASS)
ipts_list = _ipts(self.waypoints)
n = len(ipts_list)
if self.segment_widths is None:
# ── Uniform-width path (original behaviour) ──────────────────────
r = self.width // 2
r_out = r + BORDER
pygame.draw.lines(surf, C_WHITE, True, ipts_list, self.width + BORDER * 2)
for pt in ipts_list:
pygame.draw.circle(surf, C_WHITE, pt, r_out)
pygame.draw.lines(surf, C_TRACK, True, ipts_list, self.width)
for pt in ipts_list:
pygame.draw.circle(surf, C_TRACK, pt, r)
else:
# ── Variable-width path ───────────────────────────────────────────
# At each waypoint junction the circle radius is the max of the
# incoming and outgoing segment widths, ensuring no gaps at
# wide→narrow or narrow→wide transitions.
sw = self.segment_widths
# Pass 1: white outer strip
for i in range(n):
j = (i + 1) % n
w = sw[i] + BORDER * 2
w_p = sw[(i - 1) % n] + BORDER * 2
pygame.draw.line(surf, C_WHITE, ipts_list[i], ipts_list[j], w)
pygame.draw.circle(surf, C_WHITE, ipts_list[i], max(w, w_p) // 2)
# Pass 2: grey tarmac
for i in range(n):
j = (i + 1) % n
w = sw[i]
w_p = sw[(i - 1) % n]
pygame.draw.line(surf, C_TRACK, ipts_list[i], ipts_list[j], w)
pygame.draw.circle(surf, C_TRACK, ipts_list[i], max(w, w_p) // 2)
# Checkered start / finish line across the full road width
self._draw_start_finish(surf)
self.surface = surf
# Mask: covers the full road width (including border) so on_track
# returns True all the way to the white edge lines.
mask_surf = pygame.Surface((SCREEN_W, SCREEN_H))
mask_surf.fill((0, 0, 0))
if self.segment_widths is None:
r_out = self.width // 2 + BORDER
pygame.draw.lines(mask_surf, C_WHITE, True, ipts_list,
self.width + BORDER * 2)
for pt in ipts_list:
pygame.draw.circle(mask_surf, C_WHITE, pt, r_out)
else:
sw = self.segment_widths
for i in range(n):
j = (i + 1) % n
w = sw[i] + BORDER * 2
w_p = sw[(i - 1) % n] + BORDER * 2
pygame.draw.line(mask_surf, C_WHITE, ipts_list[i], ipts_list[j], w)
pygame.draw.circle(mask_surf, C_WHITE, ipts_list[i], max(w, w_p) // 2)
self.mask = mask_surf
self.hud_corner = self._best_hud_corner(330, 175)
def _draw_start_finish(self, surf):
"""
Checkered black/white flag pattern across the track at start_pos,
perpendicular to the driving direction. 2 rows Γ— N columns of 10 px cells.
"""
CELL = 10
ROWS = 2
sx, sy = self.start_pos
# Unit vectors: across the track (perp) and along the track (along)
perp_rad = math.radians(self.start_angle + 90)
along_rad = math.radians(self.start_angle)
perp = (math.cos(perp_rad), math.sin(perp_rad))
along = (math.cos(along_rad), math.sin(along_rad))
n_cols = self._start_road_width // CELL + 4 # slightly wider than road
half = n_cols / 2.0
for row in range(ROWS):
v = (row - ROWS / 2.0 + 0.5) * CELL # offset along driving dir
for col in range(-int(half) - 1, int(half) + 2):
u = col * CELL # offset across track
color = (255, 255, 255) if (row + col) % 2 == 0 else (0, 0, 0)
# Four corners of this cell in screen space
pts = []
for du, dv in [(-CELL/2, -CELL/2), (CELL/2, -CELL/2),
(CELL/2, CELL/2), (-CELL/2, CELL/2)]:
px = sx + (u + du) * perp[0] + (v + dv) * along[0]
py = sy + (u + du) * perp[1] + (v + dv) * along[1]
pts.append((int(px), int(py)))
pygame.draw.polygon(surf, color, pts)
def on_track(self, x, y):
"""Return True if pixel (x, y) is on the track mask."""
if self.mask is None:
return False
ix, iy = int(round(x)), int(round(y))
if ix < 0 or iy < 0 or ix >= SCREEN_W or iy >= SCREEN_H:
return False
color = self.mask.get_at((ix, iy))
# White = on track
return color[0] > 128
def gate_side(self, x, y):
"""
Dot product of (pos - start_pos) with start direction unit vector.
Positive = ahead of gate, negative = behind gate.
"""
dx = x - self.start_pos[0]
dy = y - self.start_pos[1]
return dx * self._gate_dx + dy * self._gate_dy
# ────────────────────────────────────────────────────────────────────────────
# Track builders
# ────────────────────────────────────────────────────────────────────────────
def _build_all_tracks():
tracks = []
# ── GROUP 1: Full ellipses ───────────────────────────────────────────────
# 1. Wide Oval
wp = _full_ellipse(450, 300, 370, 215, n=80, start_deg=90)
tracks.append(TrackDef(
level=1, name="Wide Oval",
waypoints=wp, width=115,
start_pos=(450, 515), start_angle=180, max_speed=3.0
))
# 2. Standard Oval
wp = _full_ellipse(450, 300, 330, 195, n=80, start_deg=90)
tracks.append(TrackDef(
level=2, name="Standard Oval",
waypoints=wp, width=85,
start_pos=(450, 495), start_angle=180, max_speed=3.5
))
# 3. Narrow Oval
wp = _full_ellipse(450, 300, 320, 185, n=80, start_deg=90)
tracks.append(TrackDef(
level=3, name="Narrow Oval",
waypoints=wp, width=58,
start_pos=(450, 485), start_angle=180, max_speed=3.5
))
# 4. Superspeedway
wp = _full_ellipse(450, 300, 395, 160, n=80, start_deg=90)
tracks.append(TrackDef(
level=4, name="Superspeedway",
waypoints=wp, width=85,
start_pos=(450, 460), start_angle=180, max_speed=4.5
))
# ── GROUP 2: Rounded rectangles ─────────────────────────────────────────
# 5. Rounded Rectangle
# TL corner at (250,230), TR at (650,230), BR at (650,370), BL at (250,370), r=130
# BUT with r=130, bottom of BR arc = 370+130=500, BL bottom = 370+130=500
# arcs: TL 180β†’270, TR 270β†’360, BR 0β†’90, BL 90β†’180
tl_arc = _arc(250, 230, 130, 130, 180, 270, 24) # (120,230)β†’(250,100) wait...
# TL center (250,230): 180Β° β†’ (250-130,230)=(120,230), 270Β° β†’ (250,230-130)=(250,100)
tr_arc = _arc(650, 230, 130, 130, 270, 360, 24) # (650,100)β†’(780,230)
br_arc = _arc(650, 370, 130, 130, 0, 90, 24) # (780,370)β†’(650,500)
bl_arc = _arc(250, 370, 130, 130, 90, 180, 24) # (250,500)β†’(120,370)
wp = tl_arc + tr_arc + br_arc + bl_arc
tracks.append(TrackDef(
level=5, name="Rounded Rectangle",
waypoints=wp, width=90,
start_pos=(450, 500), start_angle=180, max_speed=3.5
))
# 6. Stadium Oval
left_arc = _arc(200, 300, 120, 120, 90, 270, 24) # (200,420)β†’(200,180)
right_arc = _arc(700, 300, 120, 120, 270, 450, 24) # (700,180)β†’(700,420)
wp = left_arc + right_arc
tracks.append(TrackDef(
level=6, name="Stadium Oval",
waypoints=wp, width=80,
start_pos=(450, 420), start_angle=180, max_speed=4.0
))
# 7. Tight Rectangle
# TL=(185,195), TR=(715,195), BR=(715,405), BL=(185,405), r=65
tl_arc = _arc(185, 195, 65, 65, 180, 270, 24)
tr_arc = _arc(715, 195, 65, 65, 270, 360, 24)
br_arc = _arc(715, 405, 65, 65, 0, 90, 24)
bl_arc = _arc(185, 405, 65, 65, 90, 180, 24)
wp = tl_arc + tr_arc + br_arc + bl_arc
tracks.append(TrackDef(
level=7, name="Tight Rectangle",
waypoints=wp, width=65,
start_pos=(450, 470), start_angle=180, max_speed=3.5
))
# 8. Small Oval
wp = _full_ellipse(450, 300, 265, 165, n=80, start_deg=90)
tracks.append(TrackDef(
level=8, name="Small Oval",
waypoints=wp, width=60,
start_pos=(450, 465), start_angle=180, max_speed=3.2
))
# ── GROUP 3: Two half-arcs ───────────────────────────────────────────────
# 9. Hairpin Track
# Counter-clockwise to match all other tracks (start_angle=180Β°, facing left).
# arc2_rev: left tight hairpin (220,440)β†’(140,300)β†’(220,160)
arc2_rev = _arc(220, 300, 80, 140, 90, 270, 24)
# arc1_rev: right gentle (700,160)β†’(820,300)β†’(700,440)
arc1_rev = _arc(700, 300, 120, 140, -90, 90, 24)
wp = arc2_rev + arc1_rev
tracks.append(TrackDef(
level=9, name="Hairpin Track",
waypoints=wp, width=75,
start_pos=(460, 440), start_angle=180.0, max_speed=3.5
))
# 10. Chicane Track
# Rounded rect with chicane on bottom
tl_arc = _arc(250, 240, 100, 100, 180, 270, 24)
tr_arc = _arc(650, 240, 100, 100, 270, 360, 24)
br_arc = _arc(650, 360, 100, 100, 0, 90, 24) # ends at (650,460)
bl_arc = _arc(250, 360, 100, 100, 90, 180, 24) # starts at (250,460)
# Chicane inserted between br_arc end and bl_arc start
chicane = [(650, 460), (575, 460), (545, 498), (450, 498), (355, 498), (325, 460), (250, 460)]
wp = tl_arc + tr_arc + br_arc + chicane + bl_arc
tracks.append(TrackDef(
level=10, name="Chicane Track",
waypoints=wp, width=70,
start_pos=(450, 498), start_angle=180, max_speed=3.5
))
return tracks
TRACKS = _build_all_tracks()