Загрузка данных


import math
import random
import sys
import pygame

# =============================================================================
# НАСТРОЙКИ ЭКРАНА И КОНСТАНТЫ ПСЕВДО-3D
# =============================================================================
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
HALF_WIDTH = SCREEN_WIDTH // 2
HALF_HEIGHT = SCREEN_HEIGHT // 2

FPS = 60
ROAD_WIDTH = 2000
SEGMENT_LENGTH = 200
CAMERA_HEIGHT = 1000
DRAW_DISTANCE = 220
FOV = 60
CAMERA_DEPTH = 1.0 / math.tan((FOV / 2.0) * math.pi / 180.0)

# Цвета
COLOR_SKY = (105, 160, 240)
COLOR_MOUNTAINS = (75, 95, 140)

ROAD_LIGHT = {
    'road': (105, 105, 105),
    'grass': (20, 160, 20),
    'rumble': (240, 240, 240),
    'lane': (255, 255, 255)
}
ROAD_DARK = {
    'road': (95, 95, 95),
    'grass': (10, 135, 10),
    'rumble': (210, 30, 30),
    'lane': (95, 95, 95)
}

# =============================================================================
# СЕГМЕНТ ТРАССЫ
# =============================================================================
class Segment:
    def __init__(self, index):
        self.index = index
        self.p1 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0.0}
        self.p2 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0.0}
        self.curve = 0.0
        self.clip = SCREEN_HEIGHT
        self.sprite = None
        self.sprite_x = 0.0
        self.color = ROAD_DARK if (index // 3) % 2 else ROAD_LIGHT

    def project(self, p, cam_x, cam_y, cam_z):
        world_x, world_y, world_z = p['world']

        tx = world_x - cam_x
        ty = world_y - cam_y
        tz = world_z - cam_z

        if tz <= 0.001:
            tz = 0.001

        scale = CAMERA_DEPTH / tz
        p['scale'] = scale
        
        # Гарантируем int для всех координат экрана
        sx = int(HALF_WIDTH + (scale * tx * HALF_WIDTH))
        sy = int(HALF_HEIGHT - (scale * ty * HALF_HEIGHT))
        sw = int(scale * ROAD_WIDTH * HALF_WIDTH)
        p['screen'] = [sx, sy, max(1, sw)]

# =============================================================================
# ТРАФИК
# =============================================================================
class TrafficCar:
    def __init__(self, z, offset, speed, color):
        self.z = z
        self.offset = offset
        self.speed = speed
        self.color = color
        self.width = 650
        self.height = 360

    def update(self, dt, track_length):
        self.z = (self.z + self.speed * dt) % track_length

# =============================================================================
# ОСНОВНОЙ КЛАСС ИГРЫ
# =============================================================================
class RetroHighwayRacer:
    def __init__(self):
        pygame.init()
        pygame.font.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("Retro Highway Racer 3D")
        self.clock = pygame.time.Clock()

        # Шрифты
        self.font_speed = pygame.font.SysFont("impact", 44)
        self.font_hud = pygame.font.SysFont("monospace", 20, bold=True)
        self.font_big = pygame.font.SysFont("impact", 64)

        # Трасса
        self.segments = []
        self.track_length = 0
        self.build_track()

        # Игрок
        self.player_x = 0.0
        self.player_z = 0.0
        self.speed = 0.0
        self.max_speed = 12000.0
        self.accel = self.max_speed / 4.0
        self.breaking = -self.max_speed * 1.6
        self.decel = -self.max_speed / 5.0
        self.offroad_decel = -self.max_speed / 1.4
        self.centrifugal = 3.0

        # Состояние игры
        self.lap_time = 0.0
        self.best_lap = 0.0
        self.time_left = 40.0
        self.game_over = False
        self.msg = "GO!"
        self.msg_timer = 2.0

        self.traffic = []
        self.spawn_traffic(35)
        self.particles = []
        self.sky_offset = 0.0

    def build_track(self):
        self.segments.clear()

        def add_road(enter, hold, leave, curve, elevation):
            start_y = 0 if len(self.segments) == 0 else self.segments[-1].p2['world'][1]
            total = enter + hold + leave
            for i in range(total):
                idx = len(self.segments)
                seg = Segment(idx)
                seg.p1['world'] = [0, start_y if i == 0 else self.segments[-1].p2['world'][1], idx * SEGMENT_LENGTH]

                # Синусоидальный расчет высоты и кривизны
                if i < enter:
                    t = i / enter
                    c = curve * t
                    y = start_y + (elevation - start_y) * (1.0 - math.cos(math.pi * t)) / 2.0
                elif i < enter + hold:
                    c = curve
                    y = elevation
                else:
                    t = (i - enter - hold) / leave
                    c = curve * (1.0 - t)
                    y = elevation + (0 - elevation) * (1.0 - math.cos(math.pi * t)) / 2.0

                seg.curve = c
                seg.p2['world'] = [0, int(y), (idx + 1) * SEGMENT_LENGTH]

                # Деревья на обочине
                if idx % 10 == 0 and random.random() < 0.7:
                    seg.sprite = 'tree'
                    seg.sprite_x = -1.6 if random.random() < 0.5 else 1.6

                self.segments.append(seg)

        # Конфигурация трека
        add_road(40, 60, 40, 0, 0)
        add_road(50, 70, 50, 2.5, 1200)
        add_road(60, 80, 60, -3.0, -900)
        add_road(70, 90, 70, 0, 1800)
        add_road(50, 60, 50, 3.5, 0)
        add_road(60, 80, 60, -2.5, -1200)
        add_road(40, 40, 40, 0, 0)

        self.track_length = len(self.segments) * SEGMENT_LENGTH

    def spawn_traffic(self, count):
        colors = [
            (220, 30, 30), (30, 120, 255), (255, 200, 0),
            (240, 240, 240), (40, 180, 40), (160, 50, 220)
        ]
        lanes = [-0.65, -0.22, 0.22, 0.65]
        for _ in range(count):
            z = random.uniform(1500, self.track_length - 1000)
            self.traffic.append(TrafficCar(
                z=z,
                offset=random.choice(lanes),
                speed=random.uniform(3000, 6000),
                color=random.choice(colors)
            ))

    def find_segment(self, z):
        return self.segments[int(z / SEGMENT_LENGTH) % len(self.segments)]

    def handle_input(self, dt):
        if self.game_over:
            return

        keys = pygame.key.get_pressed()
        speed_ratio = self.speed / self.max_speed

        # Газ / Тормоз
        if keys[pygame.K_UP] or keys[pygame.K_w]:
            self.speed += self.accel * dt
        elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
            self.speed += self.breaking * dt
        else:
            self.speed += self.decel * dt

        # Руление
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.player_x -= 3.0 * speed_ratio * dt
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.player_x += 3.0 * speed_ratio * dt

        # Обочина
        if (self.player_x < -1.0 or self.player_x > 1.0) and self.speed > 500:
            self.speed += self.offroad_decel * dt
            if random.random() < 0.35:
                self.particles.append([
                    HALF_WIDTH + random.randint(-40, 40),
                    SCREEN_HEIGHT - 30,
                    random.uniform(-1, 1), -1.5,
                    random.randint(10, 18), (160, 160, 160)
                ])

        self.speed = max(0.0, min(self.speed, self.max_speed))

    def update(self, dt):
        if self.game_over:
            return

        self.handle_input(dt)
        old_z = self.player_z
        self.player_z = (self.player_z + self.speed * dt) % self.track_length

        # Финишная черта / Чекпоинт
        if self.player_z < old_z:
            if self.best_lap == 0 or self.lap_time < self.best_lap:
                self.best_lap = self.lap_time
            self.lap_time = 0.0
            self.time_left += 25.0
            self.msg = "CHECKPOINT! +25s"
            self.msg_timer = 2.0

        self.lap_time += dt
        self.time_left -= dt
        if self.msg_timer > 0:
            self.msg_timer -= dt

        if self.time_left <= 0:
            self.game_over = True
            self.speed = 0

        # Центробежный снос
        cur_seg = self.find_segment(self.player_z)
        self.player_x -= (cur_seg.curve * (self.speed / self.max_speed) * self.centrifugal) * dt
        self.sky_offset -= cur_seg.curve * (self.speed / self.max_speed) * 1.5

        # Обновление трафика и аварий
        for car in self.traffic:
            car.update(dt, self.track_length)
            if abs(car.z - self.player_z) < 140 and abs(self.player_x - car.offset) < 0.35:
                self.speed = min(self.speed * 0.25, 1200)
                for _ in range(20):
                    self.particles.append([
                        HALF_WIDTH, SCREEN_HEIGHT - 60,
                        random.uniform(-4, 4), random.uniform(-4, 0),
                        random.randint(15, 25), (255, random.randint(100, 220), 0)
                    ])

        # Частицы
        for p in self.particles[:]:
            p[0] += p[2]
            p[1] += p[3]
            p[4] -= 1
            if p[4] <= 0:
                self.particles.remove(p)

    # =========================================================================
    # РЕНДЕРИНГ
    # =========================================================================
    def draw_bg(self):
        # Небо
        pygame.draw.rect(self.screen, COLOR_SKY, (0, 0, SCREEN_WIDTH, HALF_HEIGHT))
        
        # Горы на горизонте
        offset = int(self.sky_offset) % SCREEN_WIDTH
        for base in [-SCREEN_WIDTH, 0, SCREEN_WIDTH]:
            pts = [
                (base + offset, HALF_HEIGHT),
                (base + offset + 200, HALF_HEIGHT - 90),
                (base + offset + 400, HALF_HEIGHT - 40),
                (base + offset + 650, HALF_HEIGHT - 130),
                (base + offset + 880, HALF_HEIGHT - 60),
                (base + offset + 1024, HALF_HEIGHT)
            ]
            pygame.draw.polygon(self.screen, COLOR_MOUNTAINS, pts)

    def draw_segment(self, seg):
        p1 = seg.p1['screen']
        p2 = seg.p2['screen']

        r1 = max(1, p1[2] // 6)
        r2 = max(1, p2[2] // 6)

        # Трава
        pygame.draw.polygon(self.screen, seg.color['grass'], [
            (0, p1[1]), (p1[0] - p1[2] - r1, p1[1]),
            (p2[0] - p2[2] - r2, p2[1]), (0, p2[1])
        ])
        pygame.draw.polygon(self.screen, seg.color['grass'], [
            (p1[0] + p1[2] + r1, p1[1]), (SCREEN_WIDTH, p1[1]),
            (SCREEN_WIDTH, p2[1]), (p2[0] + p2[2] + r2, p2[1])
        ])

        # Бордюр
        pygame.draw.polygon(self.screen, seg.color['rumble'], [
            (p1[0] - p1[2] - r1, p1[1]), (p1[0] - p1[2], p1[1]),
            (p2[0] - p2[2], p2[1]), (p2[0] - p2[2] - r2, p2[1])
        ])
        pygame.draw.polygon(self.screen, seg.color['rumble'], [
            (p1[0] + p1[2], p1[1]), (p1[0] + p1[2] + r1, p1[1]),
            (p2[0] + p2[2] + r2, p2[1]), (p2[0] + p2[2], p2[1])
        ])

        # Асфальт
        pygame.draw.polygon(self.screen, seg.color['road'], [
            (p1[0] - p1[2], p1[1]), (p1[0] + p1[2], p1[1]),
            (p2[0] + p2[2], p2[1]), (p2[0] - p2[2], p2[1])
        ])

        # Разметка
        if seg.color == ROAD_LIGHT:
            lw1 = max(1, p1[2] // 32)
            lw2 = max(1, p2[2] // 32)
            pygame.draw.polygon(self.screen, seg.color['lane'], [
                (p1[0] - lw1, p1[1]), (p1[0] + lw1, p1[1]),
                (p2[0] + lw2, p2[1]), (p2[0] - lw2, p2[1])
            ])

    def draw_tree(self, sx, sy, scale, clip_y):
        w = int(600 * scale * HALF_WIDTH)
        h = int(1000 * scale * HALF_WIDTH)

        # Защита от отрицательных и нулевых значений Rect
        if w < 4 or h < 6:
            return
        if sy - h >= clip_y:
            return

        trunk_w = max(2, w // 6)
        trunk_h = max(2, h // 3)
        trunk_x = sx - trunk_w // 2
        trunk_y = sy - trunk_h

        # Ствол дерева
        pygame.draw.rect(self.screen, (100, 65, 30), (trunk_x, trunk_y, trunk_w, trunk_h))

        # Листва (3 яруса)
        for i in range(3):
            layer_y = trunk_y - (i * (h // 4))
            layer_w = max(4, w - (i * (w // 4)))
            pygame.draw.polygon(self.screen, (20, 110 + i * 25, 30), [
                (sx - layer_w // 2, layer_y),
                (sx, layer_y - (h // 3)),
                (sx + layer_w // 2, layer_y)
            ])

    def draw_car(self, car, seg, clip_y):
        scale = seg.p1['scale']
        p1 = seg.p1['screen']

        sx = int(p1[0] + (scale * car.offset * ROAD_WIDTH * HALF_WIDTH))
        sy = p1[1]

        w = int(car.width * scale * HALF_WIDTH)
        h = int(car.height * scale * HALF_WIDTH)

        if w < 4 or h < 4 or sy - h >= clip_y:
            return

        # Тень
        pygame.draw.ellipse(self.screen, (20, 20, 20), (sx - w // 2, sy - max(2, h // 6), w, max(2, h // 4)))
        # Кузов
        pygame.draw.rect(self.screen, car.color, (sx - w // 2, sy - h, w, max(2, int(h * 0.75))), border_radius=max(1, w // 8))
        # Окно
        roof_w = max(2, int(w * 0.65))
        roof_h = max(2, int(h * 0.4))
        pygame.draw.rect(self.screen, (35, 35, 45), (sx - roof_w // 2, sy - h + max(1, h // 10), roof_w, roof_h))
        # Огни
        lw = max(1, w // 6)
        lh = max(1, h // 6)
        pygame.draw.rect(self.screen, (255, 30, 30), (sx - w // 2 + 2, sy - int(h * 0.35), lw, lh))
        pygame.draw.rect(self.screen, (255, 30, 30), (sx + w // 2 - lw - 2, sy - int(h * 0.35), lw, lh))

    def draw_player(self):
        x = HALF_WIDTH
        y = SCREEN_HEIGHT - 20
        w = 170
        h = 85

        tilt = 0
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            tilt = -8
        elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            tilt = 8

        # Тень
        pygame.draw.ellipse(self.screen, (15, 15, 15), (x - w // 2, y - 10, w, 24))

        # Корпус
        pts = [
            (x - w // 2 + tilt, y - 25),
            (x - w // 3, y - h),
            (x + w // 3, y - h),
            (x + w // 2 + tilt, y - 25)
        ]
        pygame.draw.polygon(self.screen, (225, 20, 40), pts)

        # Стекла
        cabin = [
            (x - w // 4, y - h + 5),
            (x - w // 6, y - h - 20),
            (x + w // 6, y - h - 20),
            (x + w // 4, y - h + 5)
        ]
        pygame.draw.polygon(self.screen, (30, 30, 40), cabin)

        # Стоп-сигналы
        pygame.draw.rect(self.screen, (30, 30, 30), (x - w // 2 + tilt, y - 30, w, 14))
        glow = (255, 30, 30) if (keys[pygame.K_DOWN] or keys[pygame.K_s]) else (180, 0, 0)
        pygame.draw.rect(self.screen, glow, (x - w // 2 + tilt + 8, y - 28, 26, 10))
        pygame.draw.rect(self.screen, glow, (x + w // 2 + tilt - 34, y - 28, 26, 10))

    def render(self):
        self.draw_bg()

        base_seg = self.find_segment(self.player_z)
        base_pct = (self.player_z % SEGMENT_LENGTH) / SEGMENT_LENGTH

        player_y = base_seg.p1['world'][1] + (base_seg.p2['world'][1] - base_seg.p1['world'][1]) * base_pct
        cam_y = CAMERA_HEIGHT + player_y

        max_y = SCREEN_HEIGHT
        x = 0
        dx = -(base_seg.curve * base_pct)

        # Проход 1: Проекция и отрисовка полотна дороги (от ближнего к горизонту)
        for n in range(DRAW_DISTANCE):
            seg = self.segments[(base_seg.index + n) % len(self.segments)]
            looped = seg.index < base_seg.index
            cam_z = self.player_z - (self.track_length if looped else 0)

            seg.project(seg.p1, (self.player_x * ROAD_WIDTH) - x, cam_y, cam_z)
            seg.project(seg.p2, (self.player_x * ROAD_WIDTH) - x - dx, cam_y, cam_z)

            x += dx
            dx += seg.curve

            p1 = seg.p1['screen']
            p2 = seg.p2['screen']

            if p2[1] >= max_y or p2[1] >= p1[1]:
                seg.clip = max_y
                continue

            self.draw_segment(seg)
            max_y = p2[1]
            seg.clip = max_y

        # Проход 2: Спрайты и трафик (от горизонта к игроку)
        for n in range(DRAW_DISTANCE - 1, -1, -1):
            seg = self.segments[(base_seg.index + n) % len(self.segments)]

            if seg.sprite == 'tree':
                sx = int(seg.p1['screen'][0] + (seg.p1['scale'] * seg.sprite_x * ROAD_WIDTH * HALF_WIDTH))
                self.draw_tree(sx, seg.p1['screen'][1], seg.p1['scale'], seg.clip)

            for car in self.traffic:
                if self.find_segment(car.z).index == seg.index:
                    self.draw_car(car, seg, seg.clip)

        # Частицы
        for p in self.particles:
            pygame.draw.circle(self.screen, p[5], (int(p[0]), int(p[1])), random.randint(2, 4))

        self.draw_player()

        # HUD
        spd_kmh = int((self.speed / self.max_speed) * 310)
        hud_spd = self.font_speed.render(f"{spd_kmh:3d} KM/H", True, (255, 230, 0))
        hud_time = self.font_hud.render(f"TIME: {max(0.0, self.time_left):04.1f}s", True, (255, 255, 255))
        hud_lap = self.font_hud.render(f"LAP:  {self.lap_time:04.1f}s", True, (220, 220, 220))
        hud_best = self.font_hud.render(f"BEST: {self.best_lap:04.1f}s" if self.best_lap > 0 else "BEST: --.-s", True, (180, 180, 180))

        self.screen.blit(hud_spd, (30, SCREEN_HEIGHT - 70))
        self.screen.blit(hud_time, (30, 25))
        self.screen.blit(hud_lap, (30, 55))
        self.screen.blit(hud_best, (30, 85))

        if self.msg_timer > 0:
            msg_s = self.font_big.render(self.msg, True, (255, 255, 255))
            self.screen.blit(msg_s, (HALF_WIDTH - msg_s.get_width() // 2, 200))

        if self.game_over:
            over_s = self.font_big.render("TIME OVER", True, (255, 40, 40))
            sub_s = self.font_hud.render("PRESS 'R' TO RESTART", True, (255, 255, 255))
            self.screen.blit(over_s, (HALF_WIDTH - over_s.get_width() // 2, HALF_HEIGHT - 40))
            self.screen.blit(sub_s, (HALF_WIDTH - sub_s.get_width() // 2, HALF_HEIGHT + 35))

        pygame.display.flip()

    def reset_game(self):
        self.player_x = 0.0
        self.player_z = 0.0
        self.speed = 0.0
        self.time_left = 40.0
        self.lap_time = 0.0
        self.game_over = False
        self.msg = "GO!"
        self.msg_timer = 2.0
        self.particles.clear()

    def run(self):
        while True:
            dt = self.clock.tick(FPS) / 1000.0
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_r and self.game_over:
                        self.reset_game()

            self.update(dt)
            self.render()

if __name__ == "__main__":
    game = RetroHighwayRacer()
    game.run()