|
|
|
@ -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() |
|
|
|
|