Загрузка данных
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()