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


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 = 240        # Дальность прорисовки (в сегментах)
FOV = 60                   # Поле зрения
CAMERA_DEPTH = 1.0 / math.tan((FOV / 2.0) * math.pi / 180.0)

# Цветовая палитра трассы
COLOR_SKY = (100, 145, 230)
COLOR_MOUNTAINS_FAR = (70, 90, 150)
COLOR_MOUNTAINS_NEAR = (50, 70, 120)

ROAD_LIGHT = {
    'road': (105, 105, 105),
    'grass': (16, 160, 16),
    'rumble': (240, 240, 240),
    'lane': (255, 255, 255)
}
ROAD_DARK = {
    'road': (97, 97, 97),
    'grass': (0, 135, 0),
    'rumble': (210, 20, 20),
    'lane': (97, 97, 97)
}

# =============================================================================
# ГЕНЕРАТОР ЗВУКОВ (8-bit Retro Synth)
# =============================================================================
class SoundEngine:
    """Генерация звуковых эффектов без внешних wav-файлов"""
    def __init__(self):
        self.enabled = False
        try:
            pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
            self.enabled = True
            self.engine_channel = pygame.mixer.Channel(0)
            self.crash_sound = self._create_noise_sound(0.4)
            self.checkpoint_sound = self._create_tone_sound(660, 0.25)
        except Exception:
            self.enabled = False

    def _create_tone_sound(self, freq, duration):
        sample_rate = 22050
        n_samples = int(sample_rate * duration)
        buf = bytearray()
        for i in range(n_samples):
            t = float(i) / sample_rate
            val = int(32767.0 * 0.3 * math.sin(2.0 * math.pi * freq * t))
            buf += val.to_bytes(2, byteorder='little', signed=True)
            buf += val.to_bytes(2, byteorder='little', signed=True)
        return pygame.mixer.Sound(buffer=bytes(buf))

    def _create_noise_sound(self, duration):
        sample_rate = 22050
        n_samples = int(sample_rate * duration)
        buf = bytearray()
        for _ in range(n_samples):
            val = random.randint(-12000, 12000)
            buf += val.to_bytes(2, byteorder='little', signed=True)
            buf += val.to_bytes(2, byteorder='little', signed=True)
        return pygame.mixer.Sound(buffer=bytes(buf))

    def play_crash(self):
        if self.enabled:
            self.crash_sound.play()

    def play_checkpoint(self):
        if self.enabled:
            self.checkpoint_sound.play()

# =============================================================================
# МАТЕМАТИКА И ПРОЕКЦИЯ СЕГМЕНТОВ
# =============================================================================
class Segment:
    def __init__(self, index):
        self.index = index
        self.p1 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0}
        self.p2 = {'world': [0, 0, 0], 'screen': [0, 0, 0], 'scale': 0}
        self.curve = 0.0
        self.clip = SCREEN_HEIGHT
        self.sprite = None         # 'tree', 'billboard', 'sign'
        self.sprite_x = 0.0        # смещение от полотна (-2.0 .. +2.0)
        self.cars = []
        self.color = ROAD_DARK if (index // 3) % 2 else ROAD_LIGHT

    def project(self, p, camera_x, camera_y, camera_z):
        world_x, world_y, world_z = p['world']
        
        # Смещение относительно камеры
        cam_x = world_x - camera_x
        cam_y = world_y - camera_y
        cam_z = world_z - camera_z

        if cam_z <= 0.001:
            cam_z = 0.001

        scale = CAMERA_DEPTH / cam_z
        p['scale'] = scale
        # Перспективное проецирование на экран
        screen_x = int(HALF_WIDTH + (scale * cam_x * HALF_WIDTH))
        screen_y = int(HALF_HEIGHT - (scale * cam_y * HALF_HEIGHT))
        screen_w = int(scale * ROAD_WIDTH * HALF_WIDTH)
        p['screen'] = [screen_x, screen_y, screen_w]

# =============================================================================
# ИИ И ТРАНСПОРТ ТРАФИКА
# =============================================================================
class TrafficCar:
    def __init__(self, z, offset, speed, color):
        self.z = z
        self.offset = offset       # -0.7 .. +0.7
        self.speed = speed
        self.percent = 0.0
        self.color = color
        self.width = 650           # Реалистичная ширина в игровых единицах
        self.height = 360

    def update(self, dt, track_length):
        self.z = (self.z + self.speed * dt) % track_length
        self.percent = (self.z % SEGMENT_LENGTH) / SEGMENT_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("Outrun 3D: Fixed Engine")
        self.clock = pygame.time.Clock()
        self.sound = SoundEngine()

        self.font_speed = pygame.font.SysFont("impact", 44)
        self.font_hud = pygame.font.SysFont("monospace", 22, bold=True)
        self.font_msg = pygame.font.SysFont("impact", 64)

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

        # Игрок
        self.player_x = 0.0        # -1.0 (левый край дороги) .. 1.0 (правый)
        self.player_z = 0.0        # Дистанция по трассе
        self.speed = 0.0
        self.max_speed = 12500.0   # Макс скорость в единицах Z в секунду
        self.accel = self.max_speed / 4.0
        self.breaking = -self.max_speed * 1.5
        self.decel = -self.max_speed / 5.0
        self.offroad_decel = -self.max_speed / 1.2
        self.centrifugal = 3.2

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

        # Трафик и окружение
        self.traffic = []
        self.spawn_traffic(40)
        self.sparks = []
        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:
                    c = curve * (i / enter)
                    t = i / enter
                    y = start_y + (elevation - start_y) * (1.0 - math.cos(math.pi * t)) / 2.0
                elif i < enter + hold:
                    c = curve
                    y = elevation
                else:
                    c = curve * (1.0 - (i - enter - hold) / leave)
                    t = (i - enter - hold) / leave
                    y = elevation + (0 - elevation) * (1.0 - math.cos(math.pi * t)) / 2.0

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

                # Расстановка декораций по обочинам
                if idx % 12 == 0 and random.random() < 0.8:
                    seg.sprite = 'tree'
                    seg.sprite_x = -1.7 if random.random() < 0.5 else 1.7
                elif idx % 25 == 0:
                    seg.sprite = 'sign'
                    seg.sprite_x = -1.5 if curve < 0 else 1.5

                self.segments.append(seg)

        # Конфигурация гоночного трека
        add_road(50, 60, 50, 0, 0)
        add_road(60, 80, 60, 2.5, 1200)     # Плавный правый поворот на холм
        add_road(60, 80, 60, -2.0, -1000)   # Спуск налево
        add_road(80, 100, 80, 3.5, 2000)    # Крутой холм
        add_road(60, 60, 60, -3.0, 0)       # Извилистый участок
        add_road(40, 40, 40, 0, -800)       # Низина
        add_road(50, 70, 50, 0, 0)          # Финишная прямая

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

    def spawn_traffic(self, count):
        colors = [
            (220, 20, 20), (30, 144, 255), (255, 215, 0),
            (255, 255, 255), (50, 205, 50), (138, 43, 226)
        ]
        lanes = [-0.6, -0.2, 0.2, 0.6]
        for _ in range(count):
            z = random.uniform(1500, self.track_length - 1000)
            lane = random.choice(lanes)
            spd = random.uniform(3000, 6500)
            col = random.choice(colors)
            self.traffic.append(TrafficCar(z, lane, spd, col))

    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.2 * speed_ratio * dt
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.player_x += 3.2 * 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.4:
                self.sparks.append([
                    HALF_WIDTH + random.randint(-40, 40),
                    SCREEN_HEIGHT - 35,
                    random.uniform(-1, 1), -1.2,
                    random.randint(10, 20), (140, 140, 140)
                ])

        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.message = "CHECKPOINT! +25s"
            self.message_timer = 2.5
            self.sound.play_checkpoint()

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

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

        # Снос центробежной силой на поворотах
        current_segment = self.find_segment(self.player_z)
        self.player_x -= (current_segment.curve * (self.speed / self.max_speed) * self.centrifugal) * dt
        self.sky_offset -= current_segment.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.3, 1500)
                self.sound.play_crash()
                for _ in range(16):
                    self.sparks.append([
                        HALF_WIDTH, SCREEN_HEIGHT - 60,
                        random.uniform(-4, 4), random.uniform(-4, -1),
                        random.randint(15, 30), (255, random.randint(100, 240), 0)
                    ])

        # Обновление частиц
        for spark in self.sparks[:]:
            spark[0] += spark[2]
            spark[1] += spark[3]
            spark[4] -= 1
            if spark[4] <= 0:
                self.sparks.remove(spark)

    # =========================================================================
    # ОТРИСОВКА МИРА
    # =========================================================================
    def draw_background(self):
        # Небо
        pygame.draw.rect(self.screen, COLOR_SKY, (0, 0, SCREEN_WIDTH, HALF_HEIGHT))
        
        # Дальние горы (параллакс эффект)
        mountain_offset = int(self.sky_offset) % SCREEN_WIDTH
        for x_base in [-SCREEN_WIDTH, 0, SCREEN_WIDTH]:
            points_far = [
                (x_base + mountain_offset, HALF_HEIGHT),
                (x_base + mountain_offset + 180, HALF_HEIGHT - 120),
                (x_base + mountain_offset + 380, HALF_HEIGHT - 60),
                (x_base + mountain_offset + 600, HALF_HEIGHT - 160),
                (x_base + mountain_offset + 840, HALF_HEIGHT - 80),
                (x_base + mountain_offset + 1024, HALF_HEIGHT)
            ]
            pygame.draw.polygon(self.screen, COLOR_MOUNTAINS_FAR, points_far)

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

        # Трава слева и справа от полотна (честные полигоны без артефактов)
        r1 = p1[2] // 6
        r2 = 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])
        ])

        # Бордюры (Rumble strips)
        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:
            lane_w1 = max(1, p1[2] // 32)
            lane_w2 = max(1, p2[2] // 32)
            # Центральная линия
            pygame.draw.polygon(self.screen, seg.color['lane'], [
                (p1[0] - lane_w1, p1[1]), (p1[0] + lane_w1, p1[1]),
                (p2[0] + lane_w2, p2[1]), (p2[0] - lane_w2, p2[1])
            ])

    def draw_tree(self, sx, sy, scale, clip_y):
        w = int(600 * scale * HALF_WIDTH)
        h = int(1000 * scale * HALF_WIDTH)
        if h <= 2 or sy - h >= clip_y:
            return

        # Ствол
        trunk_w = max(2, w // 6)
        pygame.draw.rect(self.screen, (100, 65, 30), (sx - trunk_w // 2, sy - h // 3, trunk_w, h // 3))
        # Крона (3 яруса треугольников)
        for i in range(3):
            layer_y = sy - h // 3 - (i * h // 4)
            layer_w = w - (i * w // 4)
            pygame.draw.polygon(self.screen, (20, 120 + i * 20, 30), [
                (sx - layer_w // 2, layer_y),
                (sx, layer_y - h // 3),
                (sx + layer_w // 2, layer_y)
            ])

    def draw_traffic_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 <= 3 or sy - h >= clip_y:
            return

        # Тень
        shadow_rect = pygame.Rect(sx - w // 2, sy - h // 6, w, h // 4)
        pygame.draw.ellipse(self.screen, (20, 20, 20), shadow_rect)

        # Кузов
        body_rect = (sx - w // 2, sy - h, w, int(h * 0.75))
        pygame.draw.rect(self.screen, car.color, body_rect, border_radius=max(1, w // 8))

        # Крыша/Стекло
        roof_w = int(w * 0.7)
        roof_h = int(h * 0.4)
        pygame.draw.rect(self.screen, (40, 40, 50), (sx - roof_w // 2, sy - h + max(1, h // 12), roof_w, roof_h))

        # Задние фонари
        light_w = max(2, w // 6)
        light_h = max(2, h // 6)
        pygame.draw.rect(self.screen, (255, 30, 30), (sx - w // 2 + 2, sy - int(h * 0.4), light_w, light_h))
        pygame.draw.rect(self.screen, (255, 30, 30), (sx + w // 2 - light_w - 2, sy - int(h * 0.4), light_w, light_h))

    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, 25))

        # Корпус спорткара
        points = [
            (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, (220, 20, 40), points)

        # Кабина
        cabin_points = [
            (x - w // 4, y - h + 5),
            (x - w // 6, y - h - 22),
            (x + w // 6, y - h - 22),
            (x + w // 4, y - h + 5)
        ]
        pygame.draw.polygon(self.screen, (30, 30, 40), cabin_points)

        # Задние стоп-сигналы и антикрыло
        pygame.draw.rect(self.screen, (40, 40, 40), (x - w // 2 + tilt, y - 30, w, 15))
        brake_glow = (255, 30, 30) if (keys[pygame.K_DOWN] or keys[pygame.K_s]) else (180, 0, 0)
        pygame.draw.rect(self.screen, brake_glow, (x - w // 2 + tilt + 8, y - 28, 28, 10))
        pygame.draw.rect(self.screen, brake_glow, (x + w // 2 + tilt - 36, y - 28, 28, 10))

    def render(self):
        self.draw_background()

        base_segment = self.find_segment(self.player_z)
        base_percent = (self.player_z % SEGMENT_LENGTH) / SEGMENT_LENGTH

        # Расчет вертикального положения камеры относительно холмов
        player_y = base_segment.p1['world'][1] + (base_segment.p2['world'][1] - base_segment.p1['world'][1]) * base_percent
        camera_y = CAMERA_HEIGHT + player_y

        # 1. ПЕРВЫЙ ПРОХОД: Дорога (От переднего плана к горизонту с Z-отсечением)
        max_y = SCREEN_HEIGHT
        x = 0
        dx = -(base_segment.curve * base_percent)

        for n in range(DRAW_DISTANCE):
            seg = self.segments[(base_segment.index + n) % len(self.segments)]
            looped = seg.index < base_segment.index
            camera_z = self.player_z - (self.track_length if looped else 0)

            # Проекция геометрии
            seg.project(seg.p1, (self.player_x * ROAD_WIDTH) - x, camera_y, camera_z)
            seg.project(seg.p2, (self.player_x * ROAD_WIDTH) - x - dx, camera_y, camera_z)

            x += dx
            dx += seg.curve

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

            # Отсекаем сегменты за камерой и за холмами
            if seg.p1['camera_z'] if 'camera_z' in seg.p1 else False:
                continue
            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_segment.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_traffic_car(car, seg, seg.clip)

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

        self.draw_player()

        # HUD
        speed_kmh = int((self.speed / self.max_speed) * 310)
        hud_spd = self.font_speed.render(f"{speed_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, 60))
        self.screen.blit(hud_best, (30, 95))

        # Сообщения на экране (Checkpoint / Game Over)
        if self.message_timer > 0:
            msg_surf = self.font_msg.render(self.message, True, (255, 255, 255))
            self.screen.blit(msg_surf, (HALF_WIDTH - msg_surf.get_width() // 2, 200))

        if self.game_over:
            over_surf = self.font_msg.render("TIME OVER", True, (255, 40, 40))
            sub_surf = self.font_hud.render("PRESS 'R' TO RESTART", True, (255, 255, 255))
            self.screen.blit(over_surf, (HALF_WIDTH - over_surf.get_width() // 2, HALF_HEIGHT - 40))
            self.screen.blit(sub_surf, (HALF_WIDTH - sub_surf.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 = 35.0
        self.lap_time = 0.0
        self.game_over = False
        self.message = "GO!"
        self.message_timer = 2.0
        self.sparks.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()