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


import math
import random
import sys
import pygame

# =============================================================================
# НАСТРОЙКИ ЭКРАНА И МИРА
# =============================================================================
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
FPS = 60

COLOR_GRASS = (34, 139, 34)
COLOR_ROAD = (50, 50, 50)
COLOR_BORDER = (220, 220, 220)
COLOR_RUMBLE = (200, 30, 30)
COLOR_UI_BG = (20, 20, 20, 200)

# =============================================================================
# ВСПОМОГАТЕЛЬНЫЕ МАТЕМАТИЧЕСКИЕ КЛАССЫ
# =============================================================================
class Vec2:
    __slots__ = ('x', 'y')

    def __init__(self, x=0.0, y=0.0):
        self.x = float(x)
        self.y = float(y)

    def __add__(self, o):
        return Vec2(self.x + o.x, self.y + o.y)

    def __sub__(self, o):
        return Vec2(self.x - o.x, self.y - o.y)

    def __mul__(self, s):
        return Vec2(self.x * s, self.y * s)

    def length(self):
        return math.hypot(self.x, self.y)

    def normalize(self):
        l = self.length()
        return Vec2(self.x / l, self.y / l) if l > 0.0001 else Vec2(0, 0)

    def dot(self, o):
        return self.x * o.x + self.y * o.y

# =============================================================================
# ЧАСТИЦЫ И СЛЕДЫ РЕЗИНЫ
# =============================================================================
class SmokeParticle:
    def __init__(self, pos, vel):
        self.pos = Vec2(pos.x, pos.y)
        self.vel = Vec2(vel.x * 0.2 + random.uniform(-1, 1), vel.y * 0.2 + random.uniform(-1, 1))
        self.size = random.uniform(4, 9)
        self.alpha = 180.0
        self.decay = random.uniform(3.0, 5.0)

    def update(self, dt):
        self.pos += self.vel * (dt * 60.0)
        self.alpha -= self.decay * (dt * 60.0)
        self.size += 0.2 * (dt * 60.0)

    def draw(self, surface, cam):
        if self.alpha <= 0:
            return
        sx = int(self.pos.x - cam.x + SCREEN_WIDTH // 2)
        sy = int(self.pos.y - cam.y + SCREEN_HEIGHT // 2)
        s = pygame.Surface((int(self.size * 2), int(self.size * 2)), pygame.SRCALPHA)
        col = (200, 200, 200, int(max(0, min(255, self.alpha))))
        pygame.draw.circle(s, col, (int(self.size), int(self.size)), int(self.size))
        surface.blit(s, (sx - int(self.size), sy - int(self.size)))


class SkidMark:
    def __init__(self, p1, p2, alpha=100):
        self.p1 = p1
        self.p2 = p2
        self.alpha = alpha

# =============================================================================
# ТРАССА (ГЕНЕРАТОР КОЛЬЦА С ЧЕКПОИНТАМИ)
# =============================================================================
class Track:
    def __init__(self):
        self.width = 160.0
        # Опорные контрольные точки замкнутого трека
        self.control_points = [
            Vec2(400, 300), Vec2(1000, 250), Vec2(1600, 400),
            Vec2(2000, 800), Vec2(2100, 1400), Vec2(1800, 1900),
            Vec2(1200, 2100), Vec2(800, 1700), Vec2(500, 2000),
            Vec2(200, 1500), Vec2(150, 900), Vec2(250, 500)
        ]
        self.centerline = self._interpolate_catmull_rom(self.control_points, 12)
        self.checkpoints = self.centerline[::6]

    def _interpolate_catmull_rom(self, pts, divisions):
        result = []
        n = len(pts)
        for i in range(n):
            p0 = pts[(i - 1) % n]
            p1 = pts[i]
            p2 = pts[(i + 1) % n]
            p3 = pts[(i + 2) % n]
            for step in range(divisions):
                t = step / float(divisions)
                t2 = t * t
                t3 = t2 * t
                x = 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t +
                           (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
                           (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3)
                y = 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t +
                           (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
                           (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3)
                result.append(Vec2(x, y))
        return result

    def is_on_road(self, pos):
        min_dist_sq = float('inf')
        for pt in self.centerline:
            dx = pos.x - pt.x
            dy = pos.y - pt.y
            d_sq = dx * dx + dy * dy
            if d_sq < min_dist_sq:
                min_dist_sq = d_sq
        return min_dist_sq <= ((self.width * 0.5) ** 2)

    def get_closest_index(self, pos):
        closest = 0
        min_d = float('inf')
        for idx, cp in enumerate(self.checkpoints):
            d = (pos.x - cp.x)**2 + (pos.y - cp.y)**2
            if d < min_d:
                min_d = d
                closest = idx
        return closest

    def draw(self, surface, cam, skids):
        ox = SCREEN_WIDTH // 2 - cam.x
        oy = SCREEN_HEIGHT // 2 - cam.y

        n = len(self.centerline)
        # Отрисовка полотна асфальта и бордюров
        for i in range(n):
            p_cur = self.centerline[i]
            p_next = self.centerline[(i + 1) % n]
            tangent = (p_next - p_cur).normalize()
            normal = Vec2(-tangent.y, tangent.x)

            # Точки полосы
            p1_in = p_cur + normal * (self.width * 0.5)
            p1_out = p_cur - normal * (self.width * 0.5)
            p2_in = p_next + normal * (self.width * 0.5)
            p2_out = p_next - normal * (self.width * 0.5)

            # Бордюр
            rumble_col = COLOR_RUMBLE if (i % 4 < 2) else COLOR_BORDER
            pygame.draw.polygon(surface, rumble_col, [
                (p1_in.x + ox, p1_in.y + oy),
                (p1_in.x + normal.x * 12 + ox, p1_in.y + normal.y * 12 + oy),
                (p2_in.x + normal.x * 12 + ox, p2_in.y + normal.y * 12 + oy),
                (p2_in.x + ox, p2_in.y + oy)
            ])
            pygame.draw.polygon(surface, rumble_col, [
                (p1_out.x + ox, p1_out.y + oy),
                (p1_out.x - normal.x * 12 + ox, p1_out.y - normal.y * 12 + oy),
                (p2_out.x - normal.x * 12 + ox, p2_out.y - normal.y * 12 + oy),
                (p2_out.x + ox, p2_out.y + oy)
            ])

            # Асфальт
            pygame.draw.polygon(surface, COLOR_ROAD, [
                (p1_in.x + ox, p1_in.y + oy),
                (p1_out.x + ox, p1_out.y + oy),
                (p2_out.x + ox, p2_out.y + oy),
                (p2_in.x + ox, p2_in.y + oy)
            ])

        # Линия старт/финиш
        p_start = self.centerline[0]
        tan_start = (self.centerline[1] - p_start).normalize()
        norm_start = Vec2(-tan_start.y, tan_start.x)
        pygame.draw.line(surface, (255, 255, 255),
                         (p_start.x - norm_start.x * self.width * 0.5 + ox, p_start.y - norm_start.y * self.width * 0.5 + oy),
                         (p_start.x + norm_start.x * self.width * 0.5 + ox, p_start.y + norm_start.y * self.width * 0.5 + oy), 6)

        # Следы торможения
        for sk in skids:
            pygame.draw.line(surface, (20, 20, 20),
                             (sk.p1.x + ox, sk.p1.y + oy),
                             (sk.p2.x + ox, sk.p2.y + oy), 4)

# =============================================================================
# ФИЗИЧЕСКАЯ МОДЕЛЬ МАШИНЫ (ДРИФТ, ИНЕРЦИЯ, ЗАЦЕП)
# =============================================================================
class Car:
    def __init__(self, x, y, angle, color, is_ai=False):
        self.pos = Vec2(x, y)
        self.velocity = Vec2(0, 0)
        self.angle = angle             # Угол в градусах
        self.angular_velocity = 0.0
        self.color = color
        self.is_ai = is_ai

        # Физические коэффициенты
        self.width = 46.0
        self.height = 24.0
        self.mass = 1200.0
        self.engine_power = 2200.0
        self.braking_power = 3000.0
        self.drag = 0.985
        self.cornering_grip = 0.88     # Сцепление поперечной оси

        # Состояние гонки
        self.cur_checkpoint = 0
        self.lap = 0
        self.lap_time = 0.0
        self.best_lap = 0.0
        self.drifting = False
        self.prev_wheel_left = None
        self.prev_wheel_right = None

    def update(self, dt, track, throttle, steering, brake):
        rad = math.radians(self.angle)
        forward = Vec2(math.cos(rad), math.sin(rad))
        right = Vec2(-math.sin(rad), math.cos(rad))

        # Разложение вектора скорости
        fwd_speed = self.velocity.dot(forward)
        side_speed = self.velocity.dot(right)

        # Проверка съезда на траву (падение сцепления и тяги)
        on_road = track.is_on_road(self.pos)
        grip = self.cornering_grip if on_road else 0.45
        accel_factor = 1.0 if on_road else 0.4

        # Тяга и торможение
        force = 0.0
        if throttle > 0:
            force += self.engine_power * throttle * accel_factor
        if brake > 0:
            force -= self.braking_power * brake

        # Поворот с учетом скорости движения
        turn_mult = 1.0 if fwd_speed >= 0 else -1.0
        speed_factor = min(abs(fwd_speed) / 400.0, 1.0)
        self.angle += steering * 160.0 * speed_factor * turn_mult * dt

        # Сила трения о дорогу и компенсация бокового сноса
        side_friction = right * (-side_speed * grip * 45.0)
        fwd_drive = forward * force

        accel = (fwd_drive + side_friction) * (1.0 / self.mass)
        self.velocity += accel * (dt * 60.0)
        self.velocity = self.velocity * (self.drag if on_road else 0.93)
        self.pos += self.velocity * dt

        # Определение режима заноса
        self.drifting = abs(side_speed) > 130.0 and abs(fwd_speed) > 150.0

        # Чекпоинты круга
        next_cp = (self.cur_checkpoint + 1) % len(track.checkpoints)
        target_pt = track.checkpoints[next_cp]
        dist_to_next = math.hypot(self.pos.x - target_pt.x, self.pos.y - target_pt.y)
        if dist_to_next < track.width * 0.9:
            self.cur_checkpoint = next_cp
            if self.cur_checkpoint == 0:
                self.lap += 1
                if self.best_lap == 0.0 or self.lap_time < self.best_lap:
                    self.best_lap = self.lap_time
                self.lap_time = 0.0

        self.lap_time += dt

    def get_wheel_positions(self):
        rad = math.radians(self.angle)
        fwd = Vec2(math.cos(rad), math.sin(rad))
        rgt = Vec2(-math.sin(rad), math.cos(rad))
        rear_hub = self.pos - fwd * (self.width * 0.35)
        w_left = rear_hub - rgt * (self.height * 0.45)
        w_right = rear_hub + rgt * (self.height * 0.45)
        return w_left, w_right

    def draw(self, surface, cam):
        sx = self.pos.x - cam.x + SCREEN_WIDTH // 2
        sy = self.pos.y - cam.y + SCREEN_HEIGHT // 2

        car_surf = pygame.Surface((int(self.width), int(self.height)), pygame.SRCALPHA)
        # Корпус
        pygame.draw.rect(car_surf, self.color, (0, 0, int(self.width), int(self.height)), border_radius=6)
        # Лобовое стекло
        pygame.draw.rect(car_surf, (30, 30, 40), (int(self.width * 0.4), 2, int(self.width * 0.25), int(self.height - 4)), border_radius=3)
        # Фары
        pygame.draw.circle(car_surf, (255, 240, 150), (int(self.width - 3), 4), 3)
        pygame.draw.circle(car_surf, (255, 240, 150), (int(self.width - 3), int(self.height - 4)), 3)
        # Задние фонари
        pygame.draw.circle(car_surf, (220, 20, 20), (2, 4), 3)
        pygame.draw.circle(car_surf, (220, 20, 20), (2, int(self.height - 4)), 3)

        # Поворот
        rot_surf = pygame.transform.rotate(car_surf, -self.angle)
        rect = rot_surf.get_rect(center=(int(sx), int(sy)))
        surface.blit(rot_surf, rect.topleft)

# =============================================================================
# ИИ СОПЕРНИКА
# =============================================================================
class AICarController:
    def __init__(self, car, target_offset=0.0):
        self.car = car
        self.target_offset = target_offset
        self.node_idx = 0

    def update(self, dt, track):
        n = len(track.centerline)
        target = track.centerline[(self.node_idx + 4) % n]
        cur_node = track.centerline[self.node_idx]

        # Достигли ли узла
        if math.hypot(self.car.pos.x - cur_node.x, self.car.pos.y - cur_node.y) < 140.0:
            self.node_idx = (self.node_idx + 1) % n

        # Вектор на узел с учетом смещения полосы
        to_target = target - self.car.pos
        target_angle = math.degrees(math.atan2(to_target.y, to_target.x))
        diff = (target_angle - self.car.angle + 180) % 360 - 180

        steering = max(-1.0, min(1.0, diff / 25.0))
        speed = self.car.velocity.length()

        throttle = 1.0
        brake = 0.0
        # Замедление перед резкими дугами
        if abs(diff) > 40.0 and speed > 350.0:
            throttle = 0.0
            brake = 0.6

        self.car.update(dt, track, throttle, steering, brake)

# =============================================================================
# ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ
# =============================================================================
class DriftRacingGame:
    def __init__(self):
        pygame.init()
        pygame.font.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("2D Pro Drift Grand Prix")
        self.clock = pygame.time.Clock()

        self.font_speed = pygame.font.SysFont("impact", 40)
        self.font_ui = pygame.font.SysFont("monospace", 18, bold=True)
        self.font_big = pygame.font.SysFont("impact", 68)

        self.track = Track()
        start_pt = self.track.centerline[0]
        tan_pt = (self.track.centerline[1] - start_pt).normalize()
        init_angle = math.degrees(math.atan2(tan_pt.y, tan_pt.x))

        # Создаем игрока и ботов на стартовой решетке
        self.player = Car(start_pt.x, start_pt.y, init_angle, (220, 20, 40))
        self.opponents = [
            Car(start_pt.x - 70, start_pt.y + 40, init_angle, (30, 144, 255), is_ai=True),
            Car(start_pt.x - 140, start_pt.y - 40, init_angle, (255, 200, 0), is_ai=True),
            Car(start_pt.x - 210, start_pt.y + 20, init_angle, (50, 205, 50), is_ai=True)
        ]
        self.ai_controllers = [
            AICarController(self.opponents[0], -25.0),
            AICarController(self.opponents[1], 30.0),
            AICarController(self.opponents[2], 0.0)
        ]

        self.smoke_particles = []
        self.skidmarks = []

    def handle_collisions(self):
        all_cars = [self.player] + self.opponents
        n = len(all_cars)
        for i in range(n):
            for j in range(i + 1, n):
                c1 = all_cars[i]
                c2 = all_cars[j]
                delta = c2.pos - c1.pos
                dist = delta.length()
                min_dist = (c1.height + c2.height) * 0.95
                if dist < min_dist and dist > 0.001:
                    norm = delta * (1.0 / dist)
                    overlap = min_dist - dist
                    c1.pos -= norm * (overlap * 0.5)
                    c2.pos += norm * (overlap * 0.5)
                    # Обмен импульсами
                    v_rel = c1.velocity - c2.velocity
                    impulse = norm * v_rel.dot(norm) * 0.8
                    c1.velocity -= impulse
                    c2.velocity += impulse

    def run(self):
        running = True
        while running:
            dt = self.clock.tick(FPS) / 1000.0

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
                    self.__init__()

            # Ввод игрока
            keys = pygame.key.get_pressed()
            throttle = 1.0 if (keys[pygame.K_UP] or keys[pygame.K_w]) else 0.0
            brake = 1.0 if (keys[pygame.K_DOWN] or keys[pygame.K_s]) else 0.0
            steering = 0.0
            if keys[pygame.K_LEFT] or keys[pygame.K_a]:
                steering -= 1.0
            if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
                steering += 1.0

            # Физика
            self.player.update(dt, self.track, throttle, steering, brake)
            for ai in self.ai_controllers:
                ai.update(dt, self.track)

            self.handle_collisions()

            # Следы шин и дым дрифта игрока
            wl, wr = self.player.get_wheel_positions()
            if self.player.drifting:
                if self.player.prev_wheel_left and self.player.prev_wheel_right:
                    self.skidmarks.append(SkidMark(self.player.prev_wheel_left, wl))
                    self.skidmarks.append(SkidMark(self.player.prev_wheel_right, wr))
                    if len(self.skidmarks) > 400:
                        self.skidmarks.pop(0)
                        self.skidmarks.pop(0)

                # Дым
                self.smoke_particles.append(SmokeParticle(wl, self.player.velocity))
                self.smoke_particles.append(SmokeParticle(wr, self.player.velocity))

            self.player.prev_wheel_left = wl
            self.player.prev_wheel_right = wr

            for sm in self.smoke_particles[:]:
                sm.update(dt)
                if sm.alpha <= 0:
                    self.smoke_particles.remove(sm)

            # Камера плавно следует за машиной
            cam = self.player.pos

            # ОТРИСОВКА
            self.screen.fill(COLOR_GRASS)
            self.track.draw(self.screen, cam, self.skidmarks)

            for sm in self.smoke_particles:
                sm.draw(self.screen, cam)

            for ai_car in self.opponents:
                ai_car.draw(self.screen, cam)
            self.player.draw(self.screen, cam)

            # Приборная панель (HUD)
            spd = int(self.player.velocity.length() * 0.36)
            hud_spd = self.font_speed.render(f"{spd} KM/H", True, (255, 230, 0))
            hud_lap = self.font_ui.render(f"LAP: {self.player.lap} | TIME: {self.player.lap_time:.2f}s", True, (255, 255, 255))
            hud_best = self.font_ui.render(f"BEST: {self.player.best_lap:.2f}s" if self.player.best_lap > 0 else "BEST: --.--", True, (180, 255, 180))

            ui_box = pygame.Surface((260, 110), pygame.SRCALPHA)
            ui_box.fill(COLOR_UI_BG)
            self.screen.blit(ui_box, (20, 20))
            self.screen.blit(hud_spd, (35, 25))
            self.screen.blit(hud_lap, (35, 75))
            self.screen.blit(hud_best, (35, 98))

            if self.player.drifting:
                drift_txt = self.font_speed.render("DRIFT!", True, (255, 120, 0))
                self.screen.blit(drift_txt, (SCREEN_WIDTH - 160, 30))

            pygame.display.flip()

        pygame.quit()
        sys.exit()

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