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


import math
import os
import random
import sys
from pathlib import Path

import pygame
import pgzrun

WIDTH = 800
HEIGHT = 400
TITLE = "Volleyball"

GROUND_Y = 330
NET_X = WIDTH // 2
NET_TOP = 170
NET_WIDTH = 18
GRAVITY = 0.45
BALL_GRAVITY = 0.28
JUMP_VY = -9.5
MOVE_SPEED = 4
BOT_SPEED = 3.5
BALL_RADIUS = 16
PLAYER_RADIUS = 26
WIN_SCORE = 15

FRAME_CANDIDATES = [24, 12, 8, 6, 4, 1]


def resolve_assets_dir() -> Path:
    if "__file__" in globals():
        return Path(__file__).resolve().parent
    if sys.argv and sys.argv[0]:
        return Path(sys.argv[0]).resolve().parent
    return Path.cwd()


ASSET_DIR = Path(r"C:\Users\Konstantin\Downloads\images")


def asset_path(name: str) -> Path:
    return ASSET_DIR / f"{name}.png"


def load_image(name: str) -> pygame.Surface:
    path = asset_path(name)
    if not path.exists():
        raise FileNotFoundError(f"Нет файла: {path}")
    return pygame.image.load(str(path)).convert_alpha()


def guess_frame_count(sheet: pygame.Surface) -> int:
    w, h = sheet.get_size()
    valid = [n for n in FRAME_CANDIDATES if n != 1 and w % n == 0]
    if not valid:
        return 1

    # Для горизонтальных листов кадров обычно размер кадра близок к высоте.
    # Выбираем вариант, у которого ширина кадра ближе всего к высоте листа.
    return min(valid, key=lambda n: abs((w // n) - h))


def load_sheet(name: str) -> list[pygame.Surface]:
    path = asset_path(name)
    if not path.exists():
        raise FileNotFoundError(f"Нет файла: {path}")

    sheet = pygame.image.load(str(path)).convert_alpha()

    frame_count = ANIM_FRAMES.get(name, 1)

    w, h = sheet.get_size()
    frame_w = w // frame_count

    frames = []
    for i in range(frame_count):
        frame = sheet.subsurface((i * frame_w, 0, frame_w, h)).copy()
        frames.append(frame)

    return frames


def safe_sheet(name: str, fallback: str) -> list[pygame.Surface]:
    try:
        return load_sheet(name)
    except Exception:
        print(f"[!] Нет {name}.png -> используем {fallback}.png")
        return load_sheet(fallback)


def flip(frames: list[pygame.Surface]) -> list[pygame.Surface]:
    return [pygame.transform.flip(f, True, False) for f in frames]


def scaled_first(frames: list[pygame.Surface], width: int | None = None, height: int | None = None) -> pygame.Surface:
    img = frames[0]
    if width is None and height is None:
        return img
    w, h = img.get_size()
    if width is None:
        width = int(w * (height / h))
    if height is None:
        height = int(h * (width / w))
    return pygame.transform.smoothscale(img, (width, height))

ANIM_FRAMES = {
    "player_idle": 12,
    "playerRun": 12,
    "player_jump": 15,
    "player_smash": 12,
    "player_block": 12,
    "playerReception": 12,

    "ball_idle": 10,
    "ballBounce": 12,
    "ballBounceHard": 24,
    "ballRoll": 8,
    "ballFull": 48,

    "net0": 6,          # если у сетки несколько кадров
    "beachbkgo": 1,     # фон обычно 1 кадр
    "beachbkglso": 1,
}
# Анимации
player_idle = load_sheet("player_idle")
player_run = load_sheet("playerRun")
player_jump = load_sheet("player_jump")
player_smash = load_sheet("player_smash")
player_block = load_sheet("player_block")
player_receive = safe_sheet("playerReception", "player_idle")

bot_idle = flip(player_idle)
bot_run = flip(player_run)
bot_jump = flip(player_jump)
bot_smash = flip(player_smash)
bot_block = flip(player_block)
bot_receive = flip(player_receive)

ball_idle = load_sheet("ball_idle")
ball_bounce = safe_sheet("ballBounce", "ball_idle")
ball_bounce_hard = safe_sheet("ballBounceHard", "ball_idle")
ball_roll = safe_sheet("ballRoll", "ball_idle")
ball_full = safe_sheet("ballFull", "ball_idle")

bg_right = load_sheet("beachbkgo")
bg_left = load_sheet("beachbkglso")
net_frames = load_sheet("net0")
shadow_frames = safe_sheet("shadow1", "ball_idle")

net_static = scaled_first(net_frames, width=NET_WIDTH + 10, height=HEIGHT - NET_TOP)
ball_static = scaled_first(ball_idle, width=BALL_RADIUS * 2, height=BALL_RADIUS * 2)
shadow_static = scaled_first(shadow_frames, width=48, height=16)


class Animation:
    def __init__(self, frames: list[pygame.Surface], speed: int = 5):
        self.frames = frames
        self.speed = max(1, speed)
        self.index = 0
        self.tick = 0

    def update(self):
        if len(self.frames) <= 1:
            return
        self.tick += 1
        if self.tick >= self.speed:
            self.tick = 0
            self.index = (self.index + 1) % len(self.frames)

    def image(self) -> pygame.Surface:
        return self.frames[self.index]

    def reset(self):
        self.index = 0
        self.tick = 0


class Player:
    def __init__(self, x: float, side: str, anims: dict[str, list[pygame.Surface]]):
        self.x = x
        self.y = GROUND_Y
        self.vy = 0.0
        self.side = side
        self.anims = anims
        self.state = "idle"
        self.anim = Animation(self.anims[self.state], speed=4)
        self.on_ground = True
        self.facing = 1 if side == "left" else -1
        self.action_lock = 0
        self.width = self.anims["idle"][0].get_width()
        self.height = self.anims["idle"][0].get_height()

    def set_state(self, state: str):
        if state not in self.anims:
            state = "idle"
        if state != self.state:
            self.state = state
            self.anim = Animation(self.anims[self.state], speed=4)

    def rect(self) -> pygame.Rect:
        img = self.anim.image()
        return img.get_rect(center=(int(self.x), int(self.y)))

    def body_rect(self) -> pygame.Rect:
        # Более стабильная зона столкновения, чем точный прямоугольник спрайта.
        if self.on_ground:
            return pygame.Rect(int(self.x - 22), int(self.y - 72), 44, 72)
        return pygame.Rect(int(self.x - 24), int(self.y - 80), 48, 80)

    def jump(self):
        if self.on_ground:
            self.vy = JUMP_VY
            self.on_ground = False
            self.set_state("jump")

    def update(self):
        if self.action_lock > 0:
            self.action_lock -= 1

        self.anim.update()

        if not self.on_ground:
            self.y += self.vy
            self.vy += GRAVITY
            if self.y >= GROUND_Y:
                self.y = GROUND_Y
                self.vy = 0
                self.on_ground = True
                self.set_state("idle")

    def draw(self):
        img = self.anim.image()
        screen.surface.blit(img, img.get_rect(center=(int(self.x), int(self.y))))


class Ball:
    def __init__(self):
        self.x = WIDTH / 2
        self.y = HEIGHT / 2
        self.vx = 0.0
        self.vy = 0.0
        self.base = ball_idle
        self.anim = Animation(self.base, speed=4)
        self.last_touch = None
        self.spin = 0
        self.in_play = False
    def rect(self) -> pygame.Rect:
        img = self.anim.image()
        return img.get_rect(center=(int(self.x), int(self.y)))
    @property
    def left(self):
        return self.rect().left
    @property
    def right(self):
        return self.rect().right
    @property
    def top(self):
        return self.rect().top
    @property
    def bottom(self):
        return self.rect().bottom
    def set_animation(self, frames: list[pygame.Surface], speed: int = 4):
        if self.base is not frames:
            self.base = frames
            self.anim = Animation(frames, speed=speed)
    def update(self):
        self.anim.update()
        self.x += self.vx
        self.y += self.vy
        self.vy += BALL_GRAVITY
    def draw(self):
        img = self.anim.image()
        screen.surface.blit(img, img.get_rect(center=(int(self.x), int(self.y))))



player = Player(
    160,
    "left",
    {
        "idle": player_idle,
        "run": player_run,
        "jump": player_jump,
        "smash": player_smash,
        "block": player_block,
        "receive": player_receive,
    },
)

bot = Player(
    WIDTH - 160,
    "right",
    {
        "idle": bot_idle,
        "run": bot_run,
        "jump": bot_jump,
        "smash": bot_smash,
        "block": bot_block,
        "receive": bot_receive,
    },
)

ball = Ball()

player_score = 0
bot_score = 0
match_over = False
serving_side = random.choice(["left", "right"])
serve_timer = 0
msg_timer = 0
message = ""


def court_left_limit() -> int:
    return 40


def court_right_limit() -> int:
    return WIDTH - 40


def left_half_max() -> int:
    return NET_X - 34


def right_half_min() -> int:
    return NET_X + 34


def clamp_players():
    player.x = max(court_left_limit(), min(left_half_max(), player.x))
    bot.x = max(right_half_min(), min(court_right_limit(), bot.x))


def set_message(text: str, frames: int = 120):
    global message, msg_timer
    message = text
    msg_timer = frames


def reset_ball(scoring_side: str | None = None):
    global serving_side

    # scoring_side: кто получил очко. Сервис переходит к выигравшей стороне.
    if scoring_side in {"left", "right"}:
        serving_side = scoring_side

    ball.y = 210
    ball.vy = -1.5

    if serving_side == "left":
        ball.x = 220
        ball.vx = 4.5
    else:
        ball.x = WIDTH - 220
        ball.vx = -4.5

    ball.in_play = True
    ball.last_touch = None
    ball.set_animation(ball_full, speed=5)


def start_round(after_point: bool = False):
    global serve_timer
    serve_timer = 45 if after_point else 10
    ball.in_play = False
    ball.vx = 0
    ball.vy = 0
    if serving_side == "left":
        ball.x = 220
    else:
        ball.x = WIDTH - 220
    ball.y = 230
    ball.set_animation(ball_idle, speed=5)


def award_point(side: str):
    global player_score, bot_score, match_over

    if side == "left":
        player_score += 1
        set_message("Очко игроку", 90)
    else:
        bot_score += 1
        set_message("Очко боту", 90)

    if player_score >= WIN_SCORE and player_score - bot_score >= 2:
        match_over = True
        set_message("Победа игрока. Нажми R", 9999)
        return
    if bot_score >= WIN_SCORE and bot_score - player_score >= 2:
        match_over = True
        set_message("Победа бота. Нажми R", 9999)
        return

    # Сервис переходит той стороне, которая выиграла розыгрыш.
    reset_ball(scoring_side=side)
    start_round(after_point=True)


def circle_hit(px: float, py: float, pr: float, bx: float, by: float, br: float) -> bool:
    return (px - bx) ** 2 + (py - by) ** 2 <= (pr + br) ** 2


def apply_hit(source: Player, direction: int, power_x: float, power_y: float, state: str):
    # direction: 1 -> вправо, -1 -> влево
    ball.vx = power_x * direction
    ball.vy = power_y
    ball.last_touch = source.side
    ball.set_animation(ball_bounce_hard if abs(power_x) >= 8 else ball_bounce, speed=3)
    source.set_state(state)
    source.action_lock = 10


def handle_player_input():
    if keyboard.left:
        player.x -= MOVE_SPEED
        player.facing = -1
        if player.on_ground:
            player.set_state("run")
    elif keyboard.right:
        player.x += MOVE_SPEED
        player.facing = 1
        if player.on_ground:
            player.set_state("run")
    else:
        if player.on_ground and player.action_lock == 0:
            player.set_state("idle")

    if keyboard.space and player.on_ground:
        player.jump()

    # Smash
    if keyboard.z and player.action_lock == 0:
        if circle_hit(player.x, player.y - 28, 42, ball.x, ball.y, BALL_RADIUS + 3):
            apply_hit(player, 1, 8.5, -6.5, "smash")

    # Block
    if keyboard.x and player.action_lock == 0 and player.on_ground is False:
        if abs(player.x - NET_X) < 70 and ball.x < NET_X + 30 and ball.y < GROUND_Y - 60:
            if circle_hit(player.x, player.y - 30, 44, ball.x, ball.y, BALL_RADIUS + 6):
                apply_hit(player, 1, 5.5, -4.0, "block")


def handle_bot_ai():
    # Простой AI: идёт к мячу только когда мяч на его половине, иначе возвращается на позицию.
    target_x = WIDTH - 160

    if ball.x > NET_X:
        target_x = ball.x

        if ball.y < 220 and ball.vy > 0 and bot.on_ground:
            bot.jump()

        if abs(bot.x - ball.x) < 34 and circle_hit(bot.x, bot.y - 30, 42, ball.x, ball.y, BALL_RADIUS + 3):
            # Случайный выбор между приёмом и атакой
            if ball.y < 240 and random.random() < 0.35:
                apply_hit(bot, -1, 8.3, -6.3, "smash")
            else:
                apply_hit(bot, -1, 5.5, -4.5, "receive")

    # Возвращение на исходную позицию
    if abs(bot.x - target_x) > 8:
        if bot.x < target_x:
            bot.x += BOT_SPEED
            if bot.on_ground:
                bot.set_state("run")
        else:
            bot.x -= BOT_SPEED
            if bot.on_ground:
                bot.set_state("run")
    else:
        if bot.on_ground and bot.action_lock == 0:
            bot.set_state("idle")


def ball_ground_response():
    # Земля: очко получает противоположная сторона.
    if ball.bottom >= GROUND_Y:
        if ball.x < NET_X:
            award_point("right")
        else:
            award_point("left")


def ball_net_collision():
    net_rect = pygame.Rect(NET_X - NET_WIDTH // 2, NET_TOP, NET_WIDTH, GROUND_Y - NET_TOP)
    b = ball.rect()
    if not b.colliderect(net_rect):
        return

    # Отталкивание от сетки.
    if ball.x < NET_X:
        ball.x = net_rect.left - BALL_RADIUS - 1
        ball.vx = -abs(ball.vx) * 0.8
    else:
        ball.x = net_rect.right + BALL_RADIUS + 1
        ball.vx = abs(ball.vx) * 0.8

    ball.vy *= 0.85


def ball_top_collision():
    if ball.top <= 0:
        ball.y = BALL_RADIUS + 1
        ball.vy = abs(ball.vy) * 0.85


def hit_player_ball(source: Player):
    if not ball.in_play:
        return False

    radius = PLAYER_RADIUS if source.on_ground else PLAYER_RADIUS + 6
    body_center_y = source.y - 26 if source.on_ground else source.y - 34

    if not circle_hit(source.x, body_center_y, radius, ball.x, ball.y, BALL_RADIUS):
        return False

    if source.side == "left":
        direction = 1
    else:
        direction = -1

    if source.side == "left":
        # Игрок может отбивать только когда мяч на его стороне или над сеткой рядом.
        if ball.x > NET_X + 40 and source.side == "left":
            return False
    else:
        if ball.x < NET_X - 40 and source.side == "right":
            return False

    return True


def player_ball_collision():
    if not ball.in_play:
        return

    if hit_player_ball(player):
        if keyboard.z and not player.on_ground:
            apply_hit(player, 1, 8.8, -6.8, "smash")
        elif keyboard.x and not player.on_ground and abs(player.x - NET_X) < 80:
            apply_hit(player, 1, 5.5, -4.0, "block")
        else:
            angle = -5.0 if ball.y < player.y - 20 else -3.5
            power = 6.0 if player.on_ground else 6.8
            apply_hit(player, 1, power, angle, "receive")

    if hit_player_ball(bot):
        if bot.on_ground is False and random.random() < 0.28:
            apply_hit(bot, -1, 8.4, -6.6, "smash")
        else:
            angle = -5.0 if ball.y < bot.y - 20 else -3.5
            power = 6.0 if bot.on_ground else 6.8
            apply_hit(bot, -1, power, angle, "receive")


def update_ball_state():
    # Выбор анимации мяча по движению.
    if not ball.in_play:
        ball.set_animation(ball_idle, speed=5)
        return

    if abs(ball.vx) < 0.5 and abs(ball.vy) < 0.5 and ball.bottom >= GROUND_Y - 4:
        ball.set_animation(ball_roll, speed=4)
    elif ball.bottom >= GROUND_Y - 6 and abs(ball.vy) > 4:
        ball.set_animation(ball_bounce_hard, speed=3)
    elif ball.bottom >= GROUND_Y - 6:
        ball.set_animation(ball_bounce, speed=4)
    else:
        ball.set_animation(ball_idle, speed=5)


def update_background_animation():
    # Если у фонов несколько кадров, анимируем их медленно.
    pass


def update():
    global serve_timer, msg_timer

    if match_over:
        if keyboard.r:
            restart_match()
        return

    if msg_timer > 0:
        msg_timer -= 1
        if msg_timer == 0:
            set_message("", 0)

    if serve_timer > 0:
        serve_timer -= 1
        if serve_timer == 0:
            ball.in_play = True
            reset_ball(scoring_side=serving_side)

    handle_player_input()
    handle_bot_ai()

    clamp_players()

    player.update()
    bot.update()

    if ball.in_play:
        ball.update()
        ball_net_collision()
        ball_top_collision()
        player_ball_collision()
        ball_ground_response()

    update_ball_state()


bg_tick = 0


def bg_frame(frames: list[pygame.Surface], speed: int = 12) -> pygame.Surface:
    global bg_tick
    if len(frames) <= 1:
        return frames[0]
    idx = (bg_tick // speed) % len(frames)
    return frames[idx]


def draw_half_background():
    global bg_tick
    bg_tick += 1

    left = bg_frame(bg_left, speed=12)
    right = bg_frame(bg_right, speed=12)

    left_scaled = pygame.transform.smoothscale(left, (WIDTH // 2, HEIGHT))
    right_scaled = pygame.transform.smoothscale(right, (WIDTH // 2, HEIGHT))

    screen.surface.blit(left_scaled, (0, 0))
    screen.surface.blit(right_scaled, (WIDTH // 2, 0))


def draw_shadow(entity_x: float, entity_y: float, on_ground: bool):
    if on_ground:
        shadow = pygame.transform.smoothscale(shadow_static, (50, 18))
        screen.surface.blit(shadow, shadow.get_rect(center=(int(entity_x), GROUND_Y + 4)))
    else:
        # Чем выше прыжок, тем меньше и бледнее тень.
        height = max(10, int(18 - (GROUND_Y - entity_y) * 0.05))
        width = max(26, int(50 - (GROUND_Y - entity_y) * 0.12))
        shadow = pygame.transform.smoothscale(shadow_static, (width, height))
        screen.surface.blit(shadow, shadow.get_rect(center=(int(entity_x), GROUND_Y + 4)))


def draw():
    draw_half_background()

    # Сетка
    screen.surface.blit(net_static, net_static.get_rect(midtop=(NET_X, NET_TOP)))

    # Тени
    draw_shadow(player.x, player.y, player.on_ground)
    draw_shadow(bot.x, bot.y, bot.on_ground)

    # Спрайты
    player.draw()
    bot.draw()
    ball.draw()

    # Небольшой индикатор подачи
    if serve_timer > 0 and not match_over:
        text = "Подача"
        if serving_side == "left":
            text = "Подача игрока"
        else:
            text = "Подача бота"
        screen.draw.text(text, center=(WIDTH // 2, 54), fontsize=28, color="white", shadow=(1, 1))

    screen.draw.text(f"{player_score} : {bot_score}", center=(WIDTH // 2, 24), fontsize=46, color="white", shadow=(1, 1))

    if message:
        screen.draw.text(message, center=(WIDTH // 2, 88), fontsize=26, color="yellow", shadow=(1, 1))

    if match_over:
        screen.draw.text("R — перезапуск", center=(WIDTH // 2, HEIGHT - 26), fontsize=26, color="white", shadow=(1, 1))


def restart_match():
    global player_score, bot_score, match_over, serving_side, serve_timer

    player_score = 0
    bot_score = 0
    match_over = False
    serving_side = random.choice(["left", "right"])
    serve_timer = 20
    set_message("", 0)

    player.x = 160
    player.y = GROUND_Y
    player.vy = 0
    player.on_ground = True
    player.set_state("idle")

    bot.x = WIDTH - 160
    bot.y = GROUND_Y
    bot.vy = 0
    bot.on_ground = True
    bot.set_state("idle")

    reset_ball(scoring_side=serving_side)
    start_round(after_point=False)


def on_key_down(key):
    if key == keys.SPACE and player.on_ground and not match_over:
        player.jump()
    elif key == keys.R and match_over:
        restart_match()
    elif key == keys.Z and not match_over:
        # Дублируем удар по кнопке, чтобы удобнее тестировать.
        if player.on_ground is False and abs(ball.x - player.x) < 50 and abs(ball.y - player.y) < 80:
            apply_hit(player, 1, 8.8, -6.8, "smash")
    elif key == keys.X and not match_over:
        if player.on_ground is False and abs(player.x - NET_X) < 80:
            if abs(ball.x - player.x) < 55 and abs(ball.y - player.y) < 90:
                apply_hit(player, 1, 5.5, -4.0, "block")


# Старт матча
start_round(after_point=False)

pgzrun.go()