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