Лабораторные работы по Python, ЛФИ, 1 семестр
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

288 lines
10 KiB

from __future__ import annotations
import math
from copy import deepcopy
from random import choice, uniform, randrange as rnd
from dataclasses import dataclass, field
import pygame
import typing
import numpy as np
FPS = 30
RED = 0xFF0000
BLUE = 0x0000FF
YELLOW = 0xFFC91F
GREEN = 0x00FF00
MAGENTA = 0xFF03B8
CYAN = 0x00FFCC
BLACK = (0, 0, 0)
WHITE = 0xFFFFFF
GREY = 0x7D7D7D
GAME_COLORS = [RED, BLUE, YELLOW, GREEN, MAGENTA, CYAN]
WIDTH = 800
HEIGHT = 600
@dataclass
class Ball:
position: np.array
r: float = 10
v: np.array = field(default_factory=lambda: np.array([0., 0.]))
color: int = field(default_factory=lambda: choice(GAME_COLORS))
time_to_split: typing.Optional[int] = field(default_factory=lambda: choice([None, rnd(5, 25)]))
live: int = 30
def move(self):
"""Переместить мяч по прошествии единицы времени.
Метод описывает перемещение мяча за один кадр перерисовки. То есть, обновляет значения
self.x и self.y с учетом скоростей self.vx и self.vy, силы гравитации, действующей на мяч,
и стен по краям окна (размер окна 800х600).
"""
if self.time_to_split is not None:
self.time_to_split -= 1
if self.position[1] <= 500:
self.v[1] -= 1.2
self.position += self.v * np.array([1, -1])
self.v[0] *= 0.99
else:
if np.linalg.norm(self.v) > 10 ** 0.5:
self.v *= np.array([0.5, -0.5])
self.position[1] = 499
self.live -= 1
if self.position[0] > 780:
self.v[0] *= -0.5
self.position[0] = 779
def draw(self, screen):
pygame.draw.circle(
screen,
self.color,
self.position,
self.r
)
def hittest(self, obj):
"""Функция проверяет, сталкивается ли данный объект с целью, описываемой в объекте obj.
Args:
obj: Объект, с которым проверяется столкновение.
Returns:
Возвращает True в случае столкновения мяча и цели. В противном случае возвращает False.
"""
if np.linalg.norm(self.position - obj.position) < self.r + obj.r:
return True
else:
return False
def get_split(self) -> typing.Tuple[Ball, Ball]:
ball1, ball2 = deepcopy(self), deepcopy(self)
ball1.r /= 2 ** 0.5
ball2.r /= 2 ** 0.5
ball1.time_to_split = choice([None, rnd(5, 25)])
ball2.time_to_split = choice([None, rnd(5, 25)])
v_angle = np.arctan2(*self.v)
delta_angle = 0.5
ball1.v = np.linalg.norm(self.v) * np.array([np.sin(v_angle - delta_angle), np.cos(v_angle - delta_angle)])
ball2.v = np.linalg.norm(self.v) * np.array([np.sin(v_angle + delta_angle), np.cos(v_angle + delta_angle)])
return ball1, ball2
class Gun:
def __init__(self):
self.bullet = 0
self.f2_power = 10
self.f2_on = 0
self.an = 1
self.color = GREY
self.x0 = 40
self.y0 = 450
def fire2_start(self, _event):
self.f2_on = 1
def fire2_end(self, event, game: Game):
"""Выстрел мячом.
Происходит при отпускании кнопки мыши.
Начальные значения компонент скорости мяча vx и vy зависят от положения мыши.
"""
self.bullet += 1
new_ball = Ball(position=np.array([self.x0 + self.deltas()[0], self.y0 + self.deltas()[1]]))
new_ball.r += 5
self.an = math.atan2((event.pos[1] - new_ball.position[1]), (event.pos[0] - new_ball.position[0]))
new_ball.v[0] = self.f2_power * math.cos(self.an)
new_ball.v[1] = - self.f2_power * math.sin(self.an)
game.balls.append(new_ball)
self.f2_on = 0
self.f2_power = 10
def targetting(self, event):
"""Прицеливание. Зависит от положения мыши."""
if event and event.pos[0] != 20:
self.an = math.atan((event.pos[1] - 450) / (event.pos[0] - 20))
if self.f2_on:
self.color = RED
else:
self.color = GREY
def deltas(self) -> typing.Tuple[float, float]:
length = 50 + self.f2_power
return length * math.cos(self.an), length * math.sin(self.an)
def draw(self, screen):
r, length = 10, 50 + self.f2_power
dx, dy = r * math.sin(self.an), -r * math.cos(self.an)
delta_x, delta_y = self.deltas()
points = [
(self.x0 + dx, self.y0 + dy), (self.x0 - dx, self.y0 - dy),
(self.x0 + delta_x - dx, self.y0 + delta_y - dy), (self.x0 + delta_x + dx, self.y0 + delta_y + dy)]
pygame.draw.polygon(screen, self.color, points)
def power_up(self):
if self.f2_on:
if self.f2_power < 100:
self.f2_power += 1
self.color = RED
else:
self.color = GREY
@dataclass
class Target:
x: float = field(default_factory=lambda: rnd(600, 780))
y: float = field(default_factory=lambda: rnd(300, 550))
r: float = field(default_factory=lambda: rnd(9, 50))
vx: float = field(default_factory=lambda: rnd(-4, 6))
vy: float = field(default_factory=lambda: rnd(-4, 6))
color: int = RED
points: int = 0
live: int = 1
ax: float = 0
ay: float = 0
randomness_ampl: float = field(default_factory=lambda: uniform(0, 5) * choice([0, 0, 1]))
oscillation_freq: float = field(default_factory=lambda: 0.05 * choice([-1, 1]))
oscillation_ampl: float = field(default_factory=lambda: rnd(0, 6) * choice([0, 0, 1]))
oscillation_phase: float = field(default_factory=lambda: uniform(0, 2 * np.pi))
def hit(self, points=1):
"""Попадание шарика в цель."""
self.points += points
def draw(self, screen):
pygame.draw.circle(
screen,
self.color,
(self.x, self.y),
self.r
)
pygame.draw.circle(
screen,
WHITE,
(self.x, self.y),
self.r * 0.7
)
pygame.draw.circle(
screen,
self.color if not self.oscillation_ampl and not self.randomness_ampl else BLUE,
(self.x, self.y),
self.r * 0.45
)
def move(self):
"""Переместить мяч по прошествии единицы времени.
Метод описывает перемещение мяча за один кадр перерисовки. То есть, обновляет значения
self.x и self.y с учетом скоростей self.vx и self.vy, силы гравитации, действующей на мяч,
и стен по краям окна (размер окна 800х600).
"""
if self.y < 0:
self.vy = -abs(self.vy) * 0.5 + self.ay
if self.x < 0:
self.vx = abs(self.vx) * 0.75 + self.ax
if self.y <= 500:
self.y -= self.vy + self.oscillation_ampl * np.sin(self.oscillation_phase)
self.x += self.vx + self.oscillation_ampl * np.cos(self.oscillation_phase)
self.vx *= 0.98
else:
# if self.vx ** 2 + self.vy ** 2 > 10:
# self.vy = -self.vy / 2
# self.vx = self.vx / 2
self.vy = 0.9 * abs(self.vy)
self.y = 499
if self.x > 780:
self.vx = -abs(self.vx) / 2
self.x = 779
self.oscillation_phase += self.oscillation_freq
self.ax += self.randomness_ampl * uniform(-1, 1)
self.ay += self.randomness_ampl * uniform(-1, 1)
@property
def position(self):
return np.array([self.x, self.y])
@dataclass
class Game:
balls: typing.List[Ball] = field(default_factory=list)
targets: typing.List[Target] = field(default_factory=lambda: [Target(), Target()])
gun: Gun = field(default_factory=Gun)
finished: bool = False
clock: pygame.time.Clock = field(default_factory=pygame.time.Clock)
fps: int = FPS
def move(self):
for target in self.targets:
target.move()
for ball in self.balls:
ball.move()
if ball.live < 0:
self.balls.pop([i for i in range(len(self.balls)) if self.balls[i].position is ball.position][0])
elif ball.time_to_split is not None and ball.time_to_split < 0:
ball1, ball2 = ball.get_split()
self.balls.pop([i for i in range(len(self.balls)) if self.balls[i].position is ball.position][0])
self.balls.extend([ball1, ball2])
for target in self.targets:
if ball.hittest(target) and target.live:
target.live = 0
ball.live = -1
target.hit()
self.targets.remove(target)
self.targets.append(Target())
self.gun.power_up()
def draw(self, screen):
screen.fill(WHITE)
self.gun.draw(screen)
for target in self.targets:
target.draw(screen)
for ball in self.balls:
ball.draw(screen)
pygame.display.update()
def process_event(self, event):
if event.type == pygame.QUIT:
self.finished = True
elif event.type == pygame.MOUSEBUTTONDOWN:
self.gun.fire2_start(event)
elif event.type == pygame.MOUSEBUTTONUP:
self.gun.fire2_end(event, self)
elif event.type == pygame.MOUSEMOTION:
self.gun.targetting(event)
def main_loop(self):
screen = pygame.display.set_mode((WIDTH, HEIGHT))
while not self.finished:
self.draw(screen)
self.clock.tick(self.fps)
for event in pygame.event.get():
self.process_event(event)
self.move()
pygame.quit()
pygame.init()
game = Game()
game.main_loop()