File size: 17,384 Bytes
de9fc8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
"""

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()