|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from copy import deepcopy
|
|
|
|
import time
|
|
|
|
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
|
|
|
|
pygame.init()
|
|
|
|
font = pygame.font.Font('IntroRust.otf', 25)
|
|
|
|
font2 = pygame.font.Font('IntroRust.otf', 16)
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
source: int = 0
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Gun:
|
|
|
|
gun_id: int = field(default_factory=lambda: rnd(1, 1000000))
|
|
|
|
bullet: bool = 0
|
|
|
|
f2_power: int = 10
|
|
|
|
f2_on: bool = 0
|
|
|
|
an: float = 1
|
|
|
|
color: int = GREY
|
|
|
|
x0: int = 40
|
|
|
|
y0: int = 350
|
|
|
|
bg_color: int = BLUE
|
|
|
|
keymap: typing.List[int] = field(
|
|
|
|
default_factory=lambda: [pygame.K_a, pygame.K_s, pygame.K_d, pygame.K_q, pygame.K_e]
|
|
|
|
)
|
|
|
|
|
|
|
|
def fire2_start(self, _event):
|
|
|
|
self.f2_on = True
|
|
|
|
|
|
|
|
def fire2_end(self, 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
|
|
|
|
new_ball.v[0] = self.f2_power * math.cos(self.an)
|
|
|
|
new_ball.v[1] = - self.f2_power * math.sin(self.an)
|
|
|
|
new_ball.source = self.gun_id
|
|
|
|
game.balls.append(new_ball)
|
|
|
|
self.f2_on = False
|
|
|
|
self.f2_power = 10
|
|
|
|
|
|
|
|
def targetting(self, event):
|
|
|
|
"""Прицеливание. Зависит от положения мыши."""
|
|
|
|
if event and event.pos[0] != self.x0:
|
|
|
|
self.an = math.atan2((event.pos[1] - self.y0), (event.pos[0] - self.x0))
|
|
|
|
if self.f2_on:
|
|
|
|
self.color = RED
|
|
|
|
else:
|
|
|
|
self.color = GREY
|
|
|
|
|
|
|
|
def handle_event(self, event, game: Game):
|
|
|
|
if event.type == pygame.MOUSEMOTION:
|
|
|
|
self.targetting(event)
|
|
|
|
if event.type == pygame.KEYDOWN and event.key == self.keymap[1]:
|
|
|
|
self.fire2_start(event)
|
|
|
|
if event.type == pygame.KEYUP and event.key == self.keymap[1]:
|
|
|
|
self.fire2_end(game)
|
|
|
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
|
|
self.fire2_start(event)
|
|
|
|
|
|
|
|
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)
|
|
|
|
pygame.draw.polygon(screen, self.bg_color,
|
|
|
|
[(self.x0 - 50, self.y0), (self.x0 + 50, self.y0), (self.x0 + 50, self.y0 + 35),
|
|
|
|
(self.x0 - 50, self.y0 + 35)])
|
|
|
|
pygame.draw.polygon(screen, self.bg_color,
|
|
|
|
[(self.x0 - 15, self.y0 - 10), (self.x0 + 15, self.y0 - 10), (self.x0 + 15, self.y0 + 3),
|
|
|
|
(self.x0 - 15, self.y0 + 3)])
|
|
|
|
|
|
|
|
def move(self, _game):
|
|
|
|
if pygame.key.get_pressed()[self.keymap[2]]:
|
|
|
|
self.x0 += 5
|
|
|
|
if pygame.key.get_pressed()[self.keymap[0]]:
|
|
|
|
self.x0 -= 5
|
|
|
|
if pygame.key.get_pressed()[self.keymap[3]]:
|
|
|
|
self.an -= 0.05
|
|
|
|
if pygame.key.get_pressed()[self.keymap[4]]:
|
|
|
|
self.an += 0.05
|
|
|
|
self.power_up()
|
|
|
|
|
|
|
|
def power_up(self):
|
|
|
|
if self.f2_on:
|
|
|
|
if self.f2_power < 100:
|
|
|
|
self.f2_power += 1
|
|
|
|
self.color = RED
|
|
|
|
else:
|
|
|
|
self.color = GREY
|
|
|
|
|
|
|
|
def hittest(self, obj):
|
|
|
|
if self.x0 - 50 - obj.r < obj.position[0] < self.x0 + 50 + obj.r \
|
|
|
|
and self.y0 - obj.r < obj.position[1] < self.y0 + 35 + obj.r \
|
|
|
|
and hasattr(obj, 'source') and obj.source != self.gun_id:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
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, game: Game, points=1):
|
|
|
|
"""Попадание шарика в цель."""
|
|
|
|
game.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()])
|
|
|
|
guns: typing.List[Gun] = field(default_factory=lambda: [
|
|
|
|
Gun(), Gun(x0=300, keymap=[pygame.K_j, pygame.K_k, pygame.K_l, pygame.K_u, pygame.K_o], bg_color=GREEN)
|
|
|
|
])
|
|
|
|
finished: bool = False
|
|
|
|
clock: pygame.time.Clock = field(default_factory=pygame.time.Clock)
|
|
|
|
fps: int = FPS
|
|
|
|
points: int = 0
|
|
|
|
screen: pygame.Surface = field(default_factory=lambda: pygame.display.set_mode((WIDTH, HEIGHT)))
|
|
|
|
|
|
|
|
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 gun in self.guns:
|
|
|
|
if gun.hittest(ball):
|
|
|
|
self.guns.remove(gun)
|
|
|
|
for target in self.targets:
|
|
|
|
if ball.hittest(target) and target.live:
|
|
|
|
target.live = 0
|
|
|
|
ball.live = -1
|
|
|
|
target.hit(self)
|
|
|
|
self.targets.remove(target)
|
|
|
|
self.targets.append(Target())
|
|
|
|
for gun in self.guns:
|
|
|
|
gun.move(self)
|
|
|
|
|
|
|
|
def draw(self, screen):
|
|
|
|
screen.fill(WHITE)
|
|
|
|
for gun in self.guns:
|
|
|
|
gun.draw(screen)
|
|
|
|
for target in self.targets:
|
|
|
|
target.draw(screen)
|
|
|
|
for ball in self.balls:
|
|
|
|
ball.draw(screen)
|
|
|
|
score_text = font.render(str(self.points), 1, BLACK)
|
|
|
|
screen.blit(score_text, (10, 10))
|
|
|
|
pygame.display.update()
|
|
|
|
|
|
|
|
def process_event(self, event):
|
|
|
|
if event.type == pygame.QUIT:
|
|
|
|
self.finished = True
|
|
|
|
elif event.type == pygame.MOUSEBUTTONUP:
|
|
|
|
for gun in self.guns:
|
|
|
|
gun.targetting(event)
|
|
|
|
gun.fire2_end(self)
|
|
|
|
for gun in self.guns:
|
|
|
|
gun.handle_event(event, self)
|
|
|
|
|
|
|
|
def main_loop(self):
|
|
|
|
while not self.finished:
|
|
|
|
self.draw(self.screen)
|
|
|
|
self.clock.tick(self.fps)
|
|
|
|
for event in pygame.event.get():
|
|
|
|
self.process_event(event)
|
|
|
|
self.move()
|
|
|
|
pygame.quit()
|
|
|
|
|
|
|
|
|
|
|
|
rules_text = '''
|
|
|
|
Управление левой пушкой:
|
|
|
|
a - влево, d - вправо, qe - наведение, s - стрелять
|
|
|
|
Управление правой:
|
|
|
|
jl - движение, uo - наведение, k - стрелять
|
|
|
|
|
|
|
|
Некоторые снаряды разрывные - разбиваются на 2.
|
|
|
|
Цели бывают обычные (те, которые полностью красные) - они
|
|
|
|
движутся, просто отражаясь от стенок, -
|
|
|
|
а также те, которые движутся случайно, и те, которые вращаются
|
|
|
|
|
|
|
|
Кликните, чтобы начать
|
|
|
|
'''
|
|
|
|
game = Game()
|
|
|
|
y = 100
|
|
|
|
game.screen.fill(0x2e2646)
|
|
|
|
for line in rules_text.split('\n'):
|
|
|
|
surface = font2.render(line, True, WHITE)
|
|
|
|
game.screen.blit(surface, (WIDTH / 2 - surface.get_width() / 2, y))
|
|
|
|
y += 5 + surface.get_height()
|
|
|
|
rules_surface = font2.render(rules_text, True, WHITE)
|
|
|
|
pygame.display.update()
|
|
|
|
while not any(ev.type == pygame.MOUSEBUTTONUP or ev.type == pygame.QUIT for ev in pygame.event.get()):
|
|
|
|
pass
|
|
|
|
game.main_loop()
|