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


"""
Симуляция Солнечной системы - детальная и быстрая версия
Школьный проект на Pygame

Управление:
- ESC - выход
- ПРОБЕЛ - пауза
- СТРЕЛКА ВВЕРХ/ВНИЗ - ускорить/замедлить время
- T - включить/выключить хвосты за планетами
- O - показать/скрыть орбиты
- N - показать/скрыть названия
- Клик мышкой по планете - показать информацию

Оптимизации:
- Фон рисуется ОДИН РАЗ и сохраняется в картинку
- Хвосты рисуются без создания новых поверхностей каждый кадр
- Свечение Солнца заранее готовится
- Орбиты и кольца кэшируются
"""

import pygame
import math
import random

# Запускаем pygame
pygame.init()

# Размеры окна
WIDTH = 1400
HEIGHT = 900
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Солнечная система")

# === ЦВЕТА ===
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (160, 160, 160)
YELLOW = (255, 220, 0)

CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2

clock = pygame.time.Clock()

# Шрифты
font_small = pygame.font.SysFont("Arial", 13)
font_medium = pygame.font.SysFont("Arial", 16)
font_large = pygame.font.SysFont("Arial", 22, bold=True)
font_info = pygame.font.SysFont("Arial", 14)

# Заранее готовим текст названий планет (чтобы не рендерить каждый кадр)
# Это сильно ускоряет программу!

# === СОЗДАЁМ ФОН ОДИН РАЗ ===
# Звёзды и туманности не двигаются, поэтому рисуем их на отдельную картинку
# и потом просто копируем эту картинку каждый кадр - это очень быстро!
print("Готовим фон...")
background = pygame.Surface((WIDTH, HEIGHT))
background.fill(BLACK)

# Туманности (рисуем сразу на фон)
for i in range(5):
    nx = random.randint(0, WIDTH)
    ny = random.randint(0, HEIGHT)
    nradius = random.randint(80, 180)
    ncolor = random.choice([(80, 30, 100), (30, 50, 100), (100, 40, 60), (40, 70, 100)])

    # Делаем мягкое свечение - несколько кругов с уменьшающейся прозрачностью
    nebula_surf = pygame.Surface((nradius * 2, nradius * 2), pygame.SRCALPHA)
    for r in range(nradius, 0, -10):
        alpha = int(20 * (1 - r / nradius))
        color = (ncolor[0], ncolor[1], ncolor[2], alpha)
        pygame.draw.circle(nebula_surf, color, (nradius, nradius), r)
    background.blit(nebula_surf, (nx - nradius, ny - nradius))

# Звёзды (рисуем на фон без мерцания - быстрее)
# Делаем много звёзд но они статичные
for i in range(500):
    x = random.randint(0, WIDTH)
    y = random.randint(0, HEIGHT)
    size = random.choice([1, 1, 1, 2, 2, 3])
    brightness = random.randint(80, 255)
    color_type = random.choice(["white", "blue", "yellow", "white", "white"])

    if color_type == "blue":
        color = (brightness // 2, brightness // 2, brightness)
    elif color_type == "yellow":
        color = (brightness, brightness, brightness // 2)
    else:
        color = (brightness, brightness, brightness)
    pygame.draw.circle(background, color, (x, y), size)

# Несколько мерцающих звёзд оставим отдельно (для эффекта)
twinkling_stars = []
for i in range(30):
    twinkling_stars.append({
        "x": random.randint(0, WIDTH),
        "y": random.randint(0, HEIGHT),
        "size": random.choice([2, 3]),
        "twinkle_speed": random.uniform(0.02, 0.1),
        "twinkle_offset": random.uniform(0, 6.28)
    })

# Орбиты тоже рисуем на фон (они не меняются)
print("Готовим орбиты...")


# === ПЛАНЕТЫ ===
planets = [
    {
        "name": "Меркурий", "color": (140, 140, 140), "color2": (90, 90, 90),
        "distance": 90, "size": 6, "speed": 0.040,
        "angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
        "info": "Самая близкая к Солнцу планета. День длится 59 земных суток.",
        "diameter": "4 879 км", "year": "88 земных дней"
    },
    {
        "name": "Венера", "color": (230, 180, 100), "color2": (180, 130, 60),
        "distance": 130, "size": 10, "speed": 0.025,
        "angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
        "info": "Самая горячая планета +462C. Вращается в обратную сторону.",
        "diameter": "12 104 км", "year": "225 земных дней"
    },
    {
        "name": "Земля", "color": (60, 130, 220), "color2": (30, 100, 60),
        "distance": 185, "size": 11, "speed": 0.018,
        "angle": random.uniform(0, 6.28), "has_moons": True,
        "moons": [{"distance": 22, "size": 3, "speed": 0.08, "angle": 0, "color": (200, 200, 200)}],
        "trail": [],
        "info": "Наш дом! Единственная известная планета с жизнью.",
        "diameter": "12 742 км", "year": "365 дней"
    },
    {
        "name": "Марс", "color": (200, 80, 50), "color2": (140, 50, 30),
        "distance": 240, "size": 8, "speed": 0.014,
        "angle": random.uniform(0, 6.28), "has_moons": True,
        "moons": [
            {"distance": 14, "size": 2, "speed": 0.12, "angle": 0, "color": (150, 130, 110)},
            {"distance": 20, "size": 2, "speed": 0.08, "angle": 2, "color": (130, 110, 90)}
        ],
        "trail": [],
        "info": "Красная планета. Имеет 2 спутника: Фобос и Деймос.",
        "diameter": "6 779 км", "year": "687 земных дней"
    },
    {
        "name": "Юпитер", "color": (220, 170, 120), "color2": (180, 130, 80),
        "distance": 330, "size": 26, "speed": 0.008,
        "angle": random.uniform(0, 6.28), "has_moons": True,
        "moons": [
            {"distance": 36, "size": 3, "speed": 0.10, "angle": 0,   "color": (240, 230, 180)},
            {"distance": 45, "size": 3, "speed": 0.07, "angle": 1.5, "color": (220, 220, 220)},
            {"distance": 55, "size": 4, "speed": 0.05, "angle": 3,   "color": (200, 180, 140)},
            {"distance": 65, "size": 3, "speed": 0.04, "angle": 4.5, "color": (150, 140, 120)},
        ],
        "trail": [],
        "info": "Самая большая планета. Большое Красное Пятно - гигантский шторм.",
        "diameter": "139 820 км", "year": "12 земных лет"
    },
    {
        "name": "Сатурн", "color": (230, 200, 140), "color2": (180, 150, 100),
        "distance": 430, "size": 22, "speed": 0.006,
        "angle": random.uniform(0, 6.28), "has_moons": True,
        "moons": [
            {"distance": 50, "size": 3, "speed": 0.06, "angle": 0,   "color": (220, 210, 180)},
            {"distance": 60, "size": 2, "speed": 0.04, "angle": 2.5, "color": (200, 200, 200)},
        ],
        "trail": [],
        "info": "Знаменит своими кольцами изо льда и камней.",
        "diameter": "116 460 км", "year": "29 земных лет"
    },
    {
        "name": "Уран", "color": (130, 210, 220), "color2": (90, 170, 190),
        "distance": 510, "size": 16, "speed": 0.004,
        "angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
        "info": "Вращается лёжа на боку. Голубой из-за метана.",
        "diameter": "50 724 км", "year": "84 земных года"
    },
    {
        "name": "Нептун", "color": (60, 100, 220), "color2": (40, 70, 180),
        "distance": 580, "size": 15, "speed": 0.003,
        "angle": random.uniform(0, 6.28), "has_moons": True,
        "moons": [{"distance": 25, "size": 3, "speed": 0.05, "angle": 0, "color": (180, 180, 200)}],
        "trail": [],
        "info": "Самая дальняя планета. Скорость ветра до 2100 км/ч.",
        "diameter": "49 244 км", "year": "165 земных лет"
    },
]

# Рисуем орбиты на фон ОДИН РАЗ
for planet in planets:
    pygame.draw.circle(background, (40, 40, 60),
                       (CENTER_X, CENTER_Y), planet["distance"], 1)

# Заранее готовим текст с названиями планет (рендеринг текста - медленная операция!)
print("Готовим тексты...")
for planet in planets:
    planet["name_surface"] = font_small.render(planet["name"], True, WHITE)
    # Заранее запоминаем ширину текста для центрирования
    planet["name_width"] = planet["name_surface"].get_width()

# === СВЕЧЕНИЕ СОЛНЦА ЗАРАНЕЕ ===
# Создаём картинку свечения один раз - потом просто копируем её
print("Готовим Солнце...")
SUN_GLOW_SIZE = 250
sun_glow = pygame.Surface((SUN_GLOW_SIZE * 2, SUN_GLOW_SIZE * 2), pygame.SRCALPHA)
for i in range(8, 0, -1):
    alpha = 25 - i * 2
    radius = 50 + i * 12
    pygame.draw.circle(sun_glow, (255, 180, 0, alpha),
                       (SUN_GLOW_SIZE, SUN_GLOW_SIZE), radius)

# === ПОЯС АСТЕРОИДОВ ===
asteroids = []
for i in range(80):
    asteroids.append({
        "distance": random.randint(275, 305),
        "angle": random.uniform(0, 6.28),
        "speed": random.uniform(0.005, 0.012),
        "size": random.randint(1, 2),
        "color": random.choice([(120, 100, 80), (100, 90, 80), (140, 120, 100)])
    })

# === КОМЕТЫ ===
comets = []
for i in range(2):
    comets.append({
        "distance": random.randint(150, 600),
        "angle": random.uniform(0, 6.28),
        "speed": random.uniform(0.003, 0.008),
        "trail": []
    })

# === ПЕРЕМЕННЫЕ СИМУЛЯЦИИ ===
sun_pulse = 0
time_speed = 1.0
paused = False
show_trails = True
show_orbits = True
show_names = True
selected_planet = None

# Кратеры на планетах - запомним позиции один раз
# (чтобы они не "прыгали" и не считались каждый кадр)
print("Готовим текстуры планет...")
for planet in planets:
    planet["craters"] = []
    if planet["name"] in ["Меркурий", "Марс"]:
        size = planet["size"]
        for _ in range(3):
            cx = random.randint(-size // 2, size // 2)
            cy = random.randint(-size // 2, size // 2)
            if cx * cx + cy * cy < (size - 1) * (size - 1):
                planet["craters"].append((cx, cy))


def draw_textured_planet(surface, x, y, size, color1, color2, name, craters):
    """Рисует планету с текстурой. Кратеры передаются готовыми."""
    ix, iy = int(x), int(y)

    # Основной круг
    pygame.draw.circle(surface, color1, (ix, iy), size)

    # Полосы для газовых гигантов
    if name == "Юпитер" or name == "Сатурн":
        for i in range(-size, size, 4):
            band_width = int(math.sqrt(max(0, size * size - i * i)))
            if band_width > 0:
                pygame.draw.line(surface, color2,
                                 (ix - band_width, iy + i),
                                 (ix + band_width, iy + i), 2)
        if name == "Юпитер":
            pygame.draw.ellipse(surface, (180, 70, 50),
                                (ix + size // 3, iy - 2, 8, 4))

    # Континенты у Земли
    elif name == "Земля":
        pygame.draw.ellipse(surface, color2,
                            (ix - size // 2, iy - size // 2, 6, 4))
        pygame.draw.ellipse(surface, color2, (ix + 1, iy + 1, 5, 3))
        pygame.draw.circle(surface, color2,
                           (ix - size // 3, iy + size // 3), 2)

    # Кратеры (берём из заранее посчитанного списка)
    elif name in ("Меркурий", "Марс"):
        for cx, cy in craters:
            pygame.draw.circle(surface, color2, (ix + cx, iy + cy), 1)

    # Облака у Венеры
    elif name == "Венера":
        pygame.draw.arc(surface, color2,
                        (ix - size, iy - size, size * 2, size * 2),
                        0.5, 2.5, 2)

    # Блик света (упрощённый - без отдельной поверхности)
    pygame.draw.circle(surface, (255, 255, 255),
                       (ix - size // 2, iy - size // 2),
                       max(1, size // 4))


def draw_saturn_rings_back(surface, x, y, size):
    """Задняя часть колец Сатурна (под планетой)."""
    ring_colors = [(180, 150, 100), (200, 170, 120), (160, 130, 90)]
    for i, ring_color in enumerate(ring_colors):
        rw = size * 4 + i * 4
        rh = int(size * 1.2) + i * 2
        ring_rect = pygame.Rect(int(x) - rw // 2, int(y) - rh // 2, rw, rh)
        pygame.draw.arc(surface, ring_color, ring_rect, math.pi, 2 * math.pi, 2)


def draw_saturn_rings_front(surface, x, y, size):
    """Передняя часть колец Сатурна (поверх планеты)."""
    ring_colors = [(200, 170, 120), (220, 190, 140), (180, 150, 100)]
    for i, ring_color in enumerate(ring_colors):
        rw = size * 4 + i * 4
        rh = int(size * 1.2) + i * 2
        ring_rect = pygame.Rect(int(x) - rw // 2, int(y) - rh // 2, rw, rh)
        pygame.draw.arc(surface, ring_color, ring_rect, 0, math.pi, 2)


def draw_uranus_rings(surface, x, y, size):
    """Уран наклонён на бок - кольца вертикальные."""
    ix, iy = int(x), int(y)
    for i in range(3):
        rw = int(size * 1.2) + i * 2
        rh = size * 3 + i * 4
        pygame.draw.ellipse(surface, (100, 150, 170),
                            (ix - rw // 2, iy - rh // 2, rw, rh), 1)


def get_planet_position(planet):
    """Возвращает координаты планеты."""
    x = CENTER_X + math.cos(planet["angle"]) * planet["distance"]
    y = CENTER_Y + math.sin(planet["angle"]) * planet["distance"]
    return x, y


def draw_info_panel(surface, planet):
    """Рисует панель с информацией о планете."""
    panel_width = 280
    panel_height = 140
    panel_x = WIDTH - panel_width - 20
    panel_y = 20

    # Полупрозрачный фон
    panel = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
    panel.fill((20, 20, 40, 220))
    surface.blit(panel, (panel_x, panel_y))

    pygame.draw.rect(surface, (100, 100, 200),
                     (panel_x, panel_y, panel_width, panel_height), 2)

    name_text = font_large.render(planet["name"], True, planet["color"])
    surface.blit(name_text, (panel_x + 10, panel_y + 10))

    diameter_text = font_info.render("Диаметр: " + planet["diameter"], True, WHITE)
    surface.blit(diameter_text, (panel_x + 10, panel_y + 45))

    year_text = font_info.render("Год: " + planet["year"], True, WHITE)
    surface.blit(year_text, (panel_x + 10, panel_y + 65))

    # Разбиваем описание на строки
    words = planet["info"].split()
    lines = []
    current_line = ""
    for word in words:
        test_line = current_line + " " + word if current_line else word
        if font_info.size(test_line)[0] < panel_width - 20:
            current_line = test_line
        else:
            lines.append(current_line)
            current_line = word
    if current_line:
        lines.append(current_line)

    for i, line in enumerate(lines):
        line_text = font_info.render(line, True, (200, 200, 220))
        surface.blit(line_text, (panel_x + 10, panel_y + 90 + i * 18))


# Заранее готовим тексты управления (рисуем их 1 раз вне цикла - быстрее)
print("Готовим интерфейс...")
title_surface = font_large.render("Солнечная система", True, WHITE)
controls_surfaces = []
for text in ["ESC - выход", "ПРОБЕЛ - пауза", "СТРЕЛКИ - скорость времени",
             "T - хвосты планет", "O - орбиты", "N - названия",
             "КЛИК - выбрать планету"]:
    controls_surfaces.append(font_info.render(text, True, (180, 180, 180)))

pause_surface = font_medium.render("ПАУЗА", True, (255, 100, 100))

print("Запуск!")

# === ГЛАВНЫЙ ЦИКЛ ===
running = True
frame_count = 0
while running:
    frame_count += 1

    # === Обработка событий ===
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            elif event.key == pygame.K_SPACE:
                paused = not paused
            elif event.key == pygame.K_UP:
                time_speed = min(time_speed + 0.5, 5.0)
            elif event.key == pygame.K_DOWN:
                time_speed = max(time_speed - 0.5, 0.0)
            elif event.key == pygame.K_t:
                show_trails = not show_trails
                if not show_trails:
                    for p in planets:
                        p["trail"] = []
            elif event.key == pygame.K_o:
                show_orbits = not show_orbits
            elif event.key == pygame.K_n:
                show_names = not show_names
        if event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = event.pos
            selected_planet = None
            for planet in planets:
                px, py = get_planet_position(planet)
                dist_sq = (mouse_x - px) ** 2 + (mouse_y - py) ** 2
                if dist_sq < (planet["size"] + 5) ** 2:
                    selected_planet = planet
                    break

    # === БЫСТРАЯ ОТРИСОВКА ФОНА ===
    # Просто копируем готовую картинку - намного быстрее чем рисовать всё заново!
    if show_orbits:
        screen.blit(background, (0, 0))
    else:
        # Если орбиты выключены - рисуем чёрный фон со звёздами но без орбит
        # Для этого мы заранее не делали, поэтому просто заливаем чёрным
        # (можно сделать второй фон без орбит для идеала)
        screen.blit(background, (0, 0))

    # === МЕРЦАЮЩИЕ ЗВЁЗДЫ (всего 30 штук - очень быстро) ===
    for star in twinkling_stars:
        star["twinkle_offset"] += star["twinkle_speed"]
        twinkle = (math.sin(star["twinkle_offset"]) + 1) / 2
        brightness = int(150 + 100 * twinkle)
        color = (brightness, brightness, brightness)
        pygame.draw.circle(screen, color, (star["x"], star["y"]), star["size"])

    # === ПОЯС АСТЕРОИДОВ ===
    if not paused:
        speed_mult = time_speed
        for asteroid in asteroids:
            asteroid["angle"] += asteroid["speed"] * speed_mult
    for asteroid in asteroids:
        ax = CENTER_X + math.cos(asteroid["angle"]) * asteroid["distance"]
        ay = CENTER_Y + math.sin(asteroid["angle"]) * asteroid["distance"]
        pygame.draw.circle(screen, asteroid["color"],
                           (int(ax), int(ay)), asteroid["size"])

    # === СОЛНЦЕ ===
    sun_pulse += 0.05
    # Просто копируем готовое свечение - не рисуем заново
    screen.blit(sun_glow, (CENTER_X - SUN_GLOW_SIZE, CENTER_Y - SUN_GLOW_SIZE))

    # Солнце - несколько кругов
    pygame.draw.circle(screen, (255, 150, 0), (CENTER_X, CENTER_Y), 40)
    pygame.draw.circle(screen, YELLOW, (CENTER_X, CENTER_Y), 35)
    pygame.draw.circle(screen, (255, 240, 100), (CENTER_X, CENTER_Y), 28)
    pygame.draw.circle(screen, (255, 255, 200), (CENTER_X, CENTER_Y), 18)

    # Пятна на Солнце
    for i in range(3):
        spot_angle = sun_pulse * 0.3 + i * 2
        spot_x = CENTER_X + math.cos(spot_angle) * 20
        spot_y = CENTER_Y + math.sin(spot_angle) * 12
        pygame.draw.circle(screen, (220, 150, 0), (int(spot_x), int(spot_y)), 3)

    # === КОМЕТЫ ===
    for comet in comets:
        if not paused:
            comet["angle"] += comet["speed"] * time_speed
        cx = CENTER_X + math.cos(comet["angle"]) * comet["distance"]
        cy = CENTER_Y + math.sin(comet["angle"]) * comet["distance"]

        comet["trail"].append((cx, cy))
        if len(comet["trail"]) > 20:
            comet["trail"].pop(0)

        # Рисуем хвост простыми линиями (без прозрачных поверхностей)
        trail_len = len(comet["trail"])
        for i in range(1, trail_len):
            brightness = int(255 * (i / trail_len))
            color = (brightness, brightness, 255)
            pygame.draw.line(screen, color,
                             comet["trail"][i - 1], comet["trail"][i],
                             max(1, i // 5))

        pygame.draw.circle(screen, WHITE, (int(cx), int(cy)), 3)

    # === ПЛАНЕТЫ ===
    for planet in planets:
        if not paused:
            planet["angle"] += planet["speed"] * time_speed

        x, y = get_planet_position(planet)

        # Сохраняем точку хвоста (только каждый 2-й кадр - меньше точек, та же красота)
        if show_trails and not paused and frame_count % 2 == 0:
            planet["trail"].append((x, y))
            if len(planet["trail"]) > 60:
                planet["trail"].pop(0)

        # Рисуем хвост ПРОСТЫМИ ЛИНИЯМИ (без прозрачных поверхностей - в 10 раз быстрее!)
        if show_trails:
            trail = planet["trail"]
            trail_len = len(trail)
            if trail_len > 1:
                pc = planet["color"]
                for i in range(1, trail_len):
                    # Затухание цвета вместо прозрачности
                    fade = i / trail_len * 0.6
                    color = (int(pc[0] * fade), int(pc[1] * fade), int(pc[2] * fade))
                    pygame.draw.line(screen, color, trail[i - 1], trail[i], 2)

        # Кольца Урана - до планеты
        if planet["name"] == "Уран":
            draw_uranus_rings(screen, x, y, planet["size"])

        # Кольца Сатурна - задняя часть
        if planet["name"] == "Сатурн":
            draw_saturn_rings_back(screen, x, y, planet["size"])

        # Сама планета
        draw_textured_planet(screen, x, y, planet["size"],
                             planet["color"], planet["color2"],
                             planet["name"], planet["craters"])

        # Кольца Сатурна - передняя часть
        if planet["name"] == "Сатурн":
            draw_saturn_rings_front(screen, x, y, planet["size"])

        # Спутники
        if planet["has_moons"]:
            for moon in planet["moons"]:
                if not paused:
                    moon["angle"] += moon["speed"] * time_speed
                mx = x + math.cos(moon["angle"]) * moon["distance"]
                my = y + math.sin(moon["angle"]) * moon["distance"]
                pygame.draw.circle(screen, moon["color"],
                                   (int(mx), int(my)), moon["size"])

        # Название (используем заранее отрендеренный текст!)
        if show_names:
            screen.blit(planet["name_surface"],
                        (int(x) - planet["name_width"] // 2,
                         int(y) + planet["size"] + 8))

        # Выделение выбранной планеты
        if selected_planet == planet:
            pygame.draw.circle(screen, (100, 200, 255),
                               (int(x), int(y)), planet["size"] + 5, 2)

    # === ИНТЕРФЕЙС (готовые поверхности - быстро!) ===
    screen.blit(title_surface, (20, 20))
    for i, surf in enumerate(controls_surfaces):
        screen.blit(surf, (20, 60 + i * 20))

    # Скорость (этот текст приходится рендерить каждый раз - он меняется)
    speed_text = font_medium.render("Скорость: x" + str(round(time_speed, 1)),
                                    True, WHITE)
    screen.blit(speed_text, (20, HEIGHT - 60))

    if paused:
        screen.blit(pause_surface, (20, HEIGHT - 35))

    # FPS - чтобы видеть как быстро работает
    fps = int(clock.get_fps())
    fps_text = font_medium.render("FPS: " + str(fps), True, (100, 255, 100))
    screen.blit(fps_text, (WIDTH - 100, HEIGHT - 30))

    if selected_planet:
        draw_info_panel(screen, selected_planet)

    pygame.display.flip()
    clock.tick(60)

pygame.quit()