From ea1a6ce2a37bf9c233ce30cdcc53f34870ebdc9d Mon Sep 17 00:00:00 2001 From: ennucore Date: Mon, 1 Nov 2021 14:23:29 +0300 Subject: [PATCH] Add different balls and targets --- lab5/gun.py | 286 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 165 insertions(+), 121 deletions(-) diff --git a/lab5/gun.py b/lab5/gun.py index b72ded6..eff7859 100644 --- a/lab5/gun.py +++ b/lab5/gun.py @@ -1,9 +1,12 @@ +from __future__ import annotations import math -from random import choice, randrange as rnd +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 @@ -20,26 +23,16 @@ GAME_COLORS = [RED, BLUE, YELLOW, GREEN, MAGENTA, CYAN] WIDTH = 800 HEIGHT = 600 -global balls, bullet -balls, bullet = [], 0 +@dataclass class Ball: - def __init__(self, screen: pygame.Surface, x: int = 40, y: int = 450): - """ Конструктор класса ball - - Args: - x - начальное положение мяча по горизонтали - y - начальное положение мяча по вертикали - """ - self.screen = screen - self.x = x - self.y = y - self.r = 10 - self.vx = 0 - self.vy = 0 - self.color = choice(GAME_COLORS) - self.live = 30 + 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): """Переместить мяч по прошествии единицы времени. @@ -48,52 +41,58 @@ class Ball: self.x и self.y с учетом скоростей self.vx и self.vy, силы гравитации, действующей на мяч, и стен по краям окна (размер окна 800х600). """ - if self.y <= 500: - self.vy -= 1.2 - self.y -= self.vy - self.x += self.vx - self.vx *= 0.99 + 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 self.vx ** 2 + self.vy ** 2 > 10: - self.vy =- self.vy / 2 - self.vx = self.vx / 2 - self.y = 499 - if self.live < 0: - balls.pop(balls.index(self)) - else: - self.live -= 1 - if self.x > 780: - self.vx =- self.vx / 2 - self.x = 779 - - - def draw(self): + 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( - self.screen, + screen, self.color, - (self.x, self.y), + self.position, self.r ) def hittest(self, obj): - """Функция проверяет сталкивалкивается ли данный обьект с целью, описываемой в обьекте obj. + """Функция проверяет, сталкивается ли данный объект с целью, описываемой в объекте obj. Args: - obj: Обьект, с которым проверяется столкновение. + obj: Объект, с которым проверяется столкновение. Returns: Возвращает True в случае столкновения мяча и цели. В противном случае возвращает False. """ - if abs(obj.x - self.x) <= (self.r + obj.r) and abs(obj.y - self.y) <= (self.r + obj.r): + 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, screen): - global bullet - bullet = 0 - self.screen = screen + def __init__(self): + self.bullet = 0 self.f2_power = 10 self.f2_on = 0 self.an = 1 @@ -101,23 +100,22 @@ class Gun: self.x0 = 40 self.y0 = 450 - def fire2_start(self, event): + def fire2_start(self, _event): self.f2_on = 1 - def fire2_end(self, event): + def fire2_end(self, event, game: Game): """Выстрел мячом. Происходит при отпускании кнопки мыши. Начальные значения компонент скорости мяча vx и vy зависят от положения мыши. """ - global bullet - bullet += 1 - new_ball = Ball(self.screen, x=self.x0 + self.deltas()[0], y=self.y0 + self.deltas()[1]) + 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.y), (event.pos[0] - new_ball.x)) - new_ball.vx = self.f2_power * math.cos(self.an) - new_ball.vy = - self.f2_power * math.sin(self.an) - balls.append(new_ball) + 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 @@ -130,18 +128,18 @@ class Gun: else: self.color = GREY - def deltas(self) -> typing.Union[float, float]: + 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): + 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(self.screen, self.color, 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: @@ -152,30 +150,46 @@ class Gun: self.color = GREY +@dataclass class Target: - def __init__(self, screen): - """Инициализация новой цели. """ - x = self.x = rnd(600, 780) - y = self.y = rnd(300, 550) - r = self.r = rnd(2, 50) - self.vx = rnd(2, 6) - self.vy = rnd(2, 6) - color = self.color = RED - self.screen = screen - self.points = 0 - self.live = 1 + 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): + def draw(self, screen): pygame.draw.circle( - self.screen, + 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): """Переместить мяч по прошествии единицы времени. @@ -185,60 +199,90 @@ class Target: и стен по краям окна (размер окна 800х600). """ if self.y < 0: - self.vy = -abs(self.vy) + 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.x += self.vx - self.vx *= 0.99 + 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.y = 499 + # 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 =- self.vx / 2 + 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]) -pygame.init() -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -bullet = 0 -balls = [] - -clock = pygame.time.Clock() -gun = Gun(screen) -targets = [Target(screen), Target(screen)] -finished = False - -while not finished: - screen.fill(WHITE) - gun.draw() - for target in targets: - target.draw() - target.move() - for b in balls: - b.draw() - pygame.display.update() - - clock.tick(FPS) - for event in pygame.event.get(): + +@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: - finished = True + self.finished = True elif event.type == pygame.MOUSEBUTTONDOWN: - gun.fire2_start(event) + self.gun.fire2_start(event) elif event.type == pygame.MOUSEBUTTONUP: - gun.fire2_end(event) + self.gun.fire2_end(event, self) elif event.type == pygame.MOUSEMOTION: - gun.targetting(event) - - for b in balls: - b.move() - for target in targets: - if b.hittest(target) and target.live: - target.live = 0 - target.hit() - targets.remove(target) - targets.append(Target(screen)) - gun.power_up() - -pygame.quit() + 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()