# -*- encoding: utf-8 -*-
"""Игра Астероиды"""
import pygame
import numbers
import random
import math
import time
class GlobalConstants(object):
screen_size = (640, 480)
max_fps = 60
class Constants(object):
bullet_speed = 250.0
bullet_range = 300.0
bullet_radius = 3.0
ship_radius = 10.0
# Скорость поворота, радианы/c.
ship_rotation_speed = math.pi * 2.0
# Ускорение корабля, пикселы/с^2.
ship_accel = 50
asteroids_num = 10
asteroid_max_start_speed = 30
asteroid_min_mass = 500.0
asteroid_min_start_size = 20
asteroid_max_start_size = 50
asteroid_mass_division_coef = 3.0
asteroid_explode_min_coef = 0.5
asteroid_explode_max_coef = 1.0
class Color(object):
black = ( 0, 0, 0)
white = ( 255, 255, 255)
blue = ( 50, 50, 255)
green = ( 0, 255, 0)
dkgreen = ( 0, 100, 0)
red = ( 255, 0, 0)
purple = (0xBF,0x0F,0xB5)
brown = (0x55,0x33,0x00)
yellow = ( 255, 255, 0)
background = black
ship_in_game = red
ship_waiting = green
bullet = yellow
asteroid = (127, 127, 127)
class Vector(object):
"""Вектор"""
def __init__(self, x, y):
super(Vector, self).__init__()
self.x = x
self.y = y
def __repr__(self):
"""Возвращает строку --- внутреннее представление объекта.
Нужно, чтобы в интерактивном режиме вектора показывались как
Vector(0, 0)
а не
<asteroids.Vector instance at 0x2434bd8>
"""
return "Vector({x},{y})".format(x=self.x, y=self.y)
def __add__(self, vec):
"""Сложение векторов: v1 + v2"""
assert isinstance(vec, Vector)
return Vector(self.x + vec.x, self.y + vec.y)
def __sub__(self, vec):
"""Вычитание векторов: v1 - v2"""
assert isinstance(vec, Vector)
return Vector(self.x - vec.x, self.y - vec.y)
def __mul__(self, scalar):
"""Умножение вектора на число: v * c"""
assert isinstance(scalar, numbers.Number)
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""Умножение числа на вектор: c * v"""
assert isinstance(scalar, numbers.Number)
return self * scalar
def __div__(self, scalar):
"""Деление вектора на число: v / c"""
assert isinstance(scalar, numbers.Number)
return Vector(self.x / scalar, self.y / scalar)
def __mod__(self, vec):
"""Покоординатное целочисленное деление: v1 % v2"""
assert isinstance(vec, Vector)
return Vector(self.x % vec.x, self.y % vec.y)
def __and__(self, vec):
"""Скалярное произведение: v1 & v2"""
assert isinstance(vec, Vector)
return self.x * vec.x + self.y * vec.y
def __neg__(self):
"""Унарный минус: -v"""
return Vector(-self.x, -self.y)
def rotated(self, angle):
"""Возвращает результат поворота вектора на угол angle (в радианах)
против часовой стрелки """
x = self.x * math.cos(angle) - self.y * math.sin(angle)
y = self.x * math.sin(angle) + self.y * math.cos(angle)
return Vector(x, y)
def rounded_tuple(self):
"""Округляет координаты и возвращает их в виде кортежа
Для передачи в функции рисования pygame."""
return (int(self.x), int(self.y))
def norm(self):
"""Возвращает длину вектора"""
return (self.x**2 + self.y**2)**0.5
def normalized(self):
"""Возвращает нормализованный вектор"""
length = self.norm()
if length < 1e-10:
return Vector(0, 0)
else:
return self / length
class BaseCircleObject(object):
"""Базовый класс для объектов сцены, аппроксимируемых окружностью"""
def __init__(self, position, velocity, radius):
super(BaseCircleObject, self).__init__()
self.position = position
self.velocity = velocity
self.radius = radius
def round_position(self, screen_size):
"""Переводит позицию в прямоугольник экрана
(При движении объекты могут уходить за край экрана, при этом они
должны появиться с другой стороны экрана.)
"""
self.position = \
(self.position % screen_size + screen_size) % screen_size
def update_position(self, screen_size, dt):
"""Пересчитывает где будет объект через dt секунд"""
self.position += self.velocity * dt
self.round_position(screen_size)
class Asteroid(BaseCircleObject):
"""Астероид"""
def __init__(self, position, velocity, radius, mass):
super(Asteroid, self).__init__(position, velocity, radius)
self.mass = mass
class Ship(BaseCircleObject):
"""Корабль"""
def __init__(self, position, velocity, radius):
super(Ship, self).__init__(position, velocity, radius)
self.orientation = 0
def create_bullet(self):
"""Создаёт пулю на носу корабля, со скоростью по ориентации корабля"""
direction = Vector(0, 1).rotated(self.orientation)
position = self.position + direction * Constants.ship_radius
time_to_live = Constants.bullet_range / Constants.bullet_speed
bullet = Bullet(position,
self.velocity + direction * Constants.bullet_speed,
Constants.bullet_radius, time_to_live)
return bullet
def rotate_left(self, dt):
"""Поворот корабля влево"""
self.orientation -= dt * Constants.ship_rotation_speed
def rotate_right(self, dt):
"""Поворот корабля вправо"""
self.orientation += dt * Constants.ship_rotation_speed
def accelerate(self, dt):
"""Ускорение корабля"""
direction = Vector(0, 1).rotated(self.orientation)
self.velocity += dt * Constants.ship_accel * direction
class Bullet(BaseCircleObject):
"""Пуля"""
def __init__(self, position, velocity, radius, time_to_live):
super(Bullet, self).__init__(position, velocity, radius)
self.birth_time = time.time()
self.time_to_live = time_to_live
def is_time_exceeded(self):
"""Возвращает истекло ли время жизни пули"""
return time.time() > self.birth_time + self.time_to_live
class Scene(object):
"""Сцена
Хранит рисуемые и обрабатываемые объекты."""
def __init__(self, screen_size):
super(Scene, self).__init__()
# Списки для астероидов и пуль.
self.asteroids = []
self.bullets = []
# Создаём корабль.
self.ship = Ship(screen_size / 2.0, Vector(0, 0), Constants.ship_radius)
self.ship.orientation = math.pi
# Создаём астероиды.
for i in xrange(Constants.asteroids_num):
pos = Vector(random.randint(0, screen_size.x - 1),
random.randint(0, screen_size.y - 1))
max_speed = Constants.asteroid_max_start_speed
vel = Vector(random.uniform(-max_speed, max_speed),
random.uniform(-max_speed, max_speed))
size = random.random()
radius = Constants.asteroid_min_start_size + \
size * (Constants.asteroid_max_start_size -
Constants.asteroid_min_start_size)
mass = math.pi * radius**2
self.asteroids.append(Asteroid(pos, vel, radius, mass))
def draw_asteroid(surface, asteroid, position):
"""Рисуем астероид на surface в позиции position"""
pygame.draw.circle(surface, Color.asteroid,
position.rounded_tuple(),
int(asteroid.radius) + 1, 0)
def draw_bullet(surface, bullet, position):
"""Рисуем пулю на surface в позиции position"""
pygame.draw.circle(surface, Color.bullet,
position.rounded_tuple(),
int(bullet.radius) + 1, 0)
def draw_ship_in_game(surface, ship, position):
"""Рисуем корабль в состоянии IN_GAME на surface в позиции position"""
top = position + Vector(0, ship.radius).rotated(ship.orientation)
left = position + \
Vector(0, ship.radius).rotated(ship.orientation + 5.0 / 6 * math.pi)
right = position + \
Vector(0, ship.radius).rotated(ship.orientation - 5.0 / 6 * math.pi)
pygame.draw.polygon(surface, Color.ship_in_game,
[top.rounded_tuple(), left.rounded_tuple(), right.rounded_tuple()], 5)
def draw_ship_waiting_start(surface, ship, position):
"""Рисуем корабль в состоянии WAITING_START на surface в позиции position"""
top = position + Vector(0, ship.radius).rotated(ship.orientation)
left = position + \
Vector(0, ship.radius).rotated(ship.orientation + 5.0 / 6 * math.pi)
right = position + \
Vector(0, ship.radius).rotated(ship.orientation - 5.0 / 6 * math.pi)
pygame.draw.polygon(surface, Color.ship_waiting,
[top.rounded_tuple(), left.rounded_tuple(), right.rounded_tuple()], 5)
def draw_with_duplicates(surface, screen_size, obj, draw_func):
"""Вызываем функцию рисования для 9 смещений, чтобы нарисовать все выходы
за границы экрана"""
assert isinstance(obj, BaseCircleObject)
sx, sy = screen_size.x, screen_size.y
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
pos = obj.position + Vector(sx * dx, sy * dy)
draw_func(surface, obj, pos)
def closest_vector(screen_size, pos1, pos2):
"""Возвращает радиус-вектор из первой точки во вторую в mod N
пространстве"""
sx, sy = screen_size.x, screen_size.y
ends = [(pos2 + Vector(sx * dx, sy * dy)) for dy in (-1, 0, 1)
for dx in (-1, 0, 1)]
closest_end = min(ends, key=lambda end: (end - pos1).norm())
return closest_end - pos1
def distance(screen_size, pos1, pos2):
"""Возвращает расстояние между точками в mod N пространстве"""
return closest_vector(screen_size, pos1, pos2).norm()
def is_collides(screen_size, obj1, obj2):
"""Проверяет, сталкиваются ли два объекта-окружности"""
dist = distance(screen_size, obj1.position, obj2.position)
return dist < obj1.radius + obj2.radius
def collide_asteroids(screen_size, asteroid1, asteroid2):
"""Сталкивает два астероида"""
if is_collides(screen_size, asteroid1, asteroid2):
closest_vec = closest_vector(screen_size,
asteroid1.position, asteroid2.position)
# Столкновение.
vel1_local = asteroid1.velocity - asteroid2.velocity
if vel1_local.norm() < 1e-10:
# Астероиды двигаются параллельно.
return
if closest_vec.norm() < 1e-10:
# Астероиды в одной точке, пропустим этот случай (TODO).
return
collision_vec = closest_vec.normalized()
vel1_x_local = (vel1_local & collision_vec) * collision_vec
vel1_y_local = vel1_local - vel1_x_local
mass_sum = asteroid1.mass + asteroid2.mass
res_vel1_x_local = \
(asteroid1.mass - asteroid2.mass) / mass_sum * vel1_x_local
res_vel1_y_local = vel1_y_local
res_vel1_local = res_vel1_x_local + res_vel1_y_local
res_vel2_local = \
2 * asteroid1.mass / mass_sum * vel1_x_local
res_vel1 = res_vel1_local + asteroid2.velocity
res_vel2 = res_vel2_local + asteroid2.velocity
asteroid1.velocity = res_vel1
asteroid2.velocity = res_vel2
# Сдвигаем астероиды так, чтобы их окружности больше не пересекались.
inters_dist = asteroid1.radius + asteroid2.radius - closest_vec.norm()
asteroid1.position += -collision_vec * (inters_dist + 0.1)
asteroid1.round_position(screen_size)
asteroid2.position += collision_vec * (inters_dist + 0.1)
asteroid2.round_position(screen_size)
def explode_asteroid(screen_size, asteroid, bullet):
"""Создаём новые астероиды меньшего размера на месте взорвавшегося
старого
Возвращаем список из созданных новых маленьких астероидов.
"""
if asteroid.mass < Constants.asteroid_min_mass:
# Слишком маленький астероид --- полностью уничтожаем.
return []
else:
# Делим астероид на два меньших размером.
mass = asteroid.mass / Constants.asteroid_mass_division_coef
radius = (mass / math.pi)**0.5
part1_dir = bullet.velocity.normalized().rotated(+math.pi / 2.0)
part2_dir = bullet.velocity.normalized().rotated(-math.pi / 2.0)
vel_factor = random.uniform(
Constants.asteroid_explode_min_coef,
Constants.asteroid_explode_max_coef)
vel1 = asteroid.velocity + part1_dir * asteroid.velocity.norm() * \
vel_factor
vel2 = asteroid.velocity + part2_dir * asteroid.velocity.norm() * \
vel_factor
pos1 = asteroid.position + part1_dir * radius
pos2 = asteroid.position + part2_dir * radius
asteroid1 = Asteroid(pos1, vel1, radius, mass)
asteroid2 = Asteroid(pos2, vel2, radius, mass)
return [asteroid1, asteroid2]
class State(object):
"""Перечисление состояний игры"""
WAITING_START = 0 # Ожидаем, пока пользователь нажмёт пробел и корабль
# войдёт в игру.
IN_GAME = 1 # Корабль в игре.
class Game(object):
"""Игра"""
def __init__(self, screen_size):
super(Game, self).__init__()
self._screen_size = Vector(screen_size[0], screen_size[1])
self._scene = Scene(self._screen_size)
self._state = State.WAITING_START
# Словарь { кнопка: нажата ли }.
self._keys_state = {}
def draw(self, screen):
"""Отрисовка сцены"""
# Отрисуем фон.
self._draw_background(screen)
# Отрисуем астероиды.
self._draw_asteroids(screen)
# Отрисуем пули.
self._draw_bullets(screen)
if self._state == State.WAITING_START:
# Рисуем корабль не активным, т.к. игра ещё не начата.
draw_with_duplicates(screen, self._screen_size,
self._scene.ship, draw_ship_waiting_start)
else:
# Рисуем корабль.
assert self._state == State.IN_GAME
draw_with_duplicates(screen, self._screen_size,
self._scene.ship, draw_ship_in_game)
def update(self, dt):
"""Обновляем состояние игры на время dt.
Возвращает:
True, если игра продолжается,
False, если игру необходимо завершить.
"""
# Список произошедших внешних событий.
events = list(pygame.event.get())
# Обработка общих событий для всех состояний игры.
for event in events:
if (event.type == pygame.QUIT or
(event.type == pygame.KEYDOWN and
event.key == pygame.K_ESCAPE)):
# Нажат крест на окне или Escape.
# Прерываем обновление и возвращаем флаг, что необходимо
# завершить программу
return False
# Обрабатываем события нажатия/отжатия кнопок, чтобы всегда иметь
# общее состояние клавиатуры.
if event.type == pygame.KEYDOWN:
self._keys_state[event.key] = True
if event.type == pygame.KEYUP:
self._keys_state[event.key] = False
# Общая для всех состояний логика: обновить положения астероидов,
# обновить положения пуль.
self._move_asteroids(dt)
self._collide_asteroids()
self._move_bullets(dt)
self._update_bullets()
self._collide_bullets_with_asteroids()
self._update_ship_orientation(dt)
# Вызываем функцию обновления, соответствующую состоянию игры.
if self._state == State.WAITING_START:
self._update_waiting_start(dt, events)
else:
assert self._state == State.IN_GAME
self._update_in_game(dt, events)
# Возвращаем флаг, что игру необходимо продолжать
return True
def _is_key_down(self, key):
"""Возвращаем состояние кнопки"""
return self._keys_state.setdefault(key, False)
def _change_state_to_in_game(self):
"""Функция перехода из состояния AWAITING_START в IN_GAME"""
self._state = State.IN_GAME
def _change_state_to_waiting_start(self):
"""Функция перехода из состояния IN_GAME в AWAITING_START"""
self._state = State.WAITING_START
self._scene.ship.position = self._screen_size / 2.0
self._scene.ship.orientation = math.pi
self._scene.ship.velocity = Vector(0, 0)
def _update_waiting_start(self, dt, events):
"""Обновление сцены и обработка событий на следующие dt секунд для
состояния, когда игра ещё не началась
"""
for event in events:
if (event.type == pygame.KEYDOWN and
event.key == pygame.K_SPACE):
# Нажат пробел --- переходим в состояние игры.
self._change_state_to_in_game()
return
def _update_in_game(self, dt, events):
"""Обновление сцены и обработка событий на следующие dt секунд для
состояния, когда игра идёт
"""
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
# Был нажат пробел --- стреляем.
self._scene.bullets.append(
self._scene.ship.create_bullet())
self._update_ship_acceleration(dt)
self._move_ship(dt)
self._collide_asteroids_with_ship()
def _move_ship(self, dt):
"""Обновляем положение корабля"""
self._scene.ship.update_position(self._screen_size, dt)
def _update_ship_orientation(self, dt):
"""Обработка нажатия кнопок для поворота корабля"""
if self._is_key_down(pygame.K_LEFT):
# Поворачиваем корабль влево.
self._scene.ship.rotate_left(dt)
elif self._is_key_down(pygame.K_RIGHT):
# Поворачиваем корабль вправо.
self._scene.ship.rotate_right(dt)
def _update_ship_acceleration(self, dt):
"""Обработка нажатия кнопок для ускорения корабля"""
if self._is_key_down(pygame.K_UP):
# Ускориться.
self._scene.ship.accelerate(dt)
def _move_asteroids(self, dt):
"""Обновляем положения астероидов"""
for asteroid in self._scene.asteroids:
asteroid.update_position(self._screen_size, dt)
def _collide_asteroids(self):
"""Сталкиваем астероиды друг с другом"""
for i, asteroid1 in enumerate(self._scene.asteroids):
for asteroid2 in self._scene.asteroids[i + 1:]:
collide_asteroids(self._screen_size, asteroid1, asteroid2)
def _move_bullets(self, dt):
"""Обновляем положение пуль"""
for bullet in self._scene.bullets:
bullet.update_position(self._screen_size, dt)
def _update_bullets(self):
"""Удаляем пули, чье время жизни истекло"""
# Функция filter(func, iter) проходит по всем элементам iter и
# записывает в результирующий список лишь те элементы, для которых
# func вернёт True.
self._scene.bullets = filter(
lambda bullet: not bullet.is_time_exceeded(), self._scene.bullets)
def _collide_bullets_with_asteroids(self):
"""Сталкиваем пули и астероиды"""
new_asteroids = []
for asteroid in self._scene.asteroids:
for i, bullet in enumerate(self._scene.bullets):
if is_collides(self._screen_size, asteroid, bullet):
new_asteroids.extend(
explode_asteroid(self._screen_size,
asteroid, bullet))
del self._scene.bullets[i]
break
else:
new_asteroids.append(asteroid)
self._scene.asteroids = new_asteroids
def _collide_asteroids_with_ship(self):
"""Проверяем, не столкнулися ли корабль с астероидом"""
for asteroid in self._scene.asteroids:
if is_collides(self._screen_size, asteroid, self._scene.ship):
self._change_state_to_waiting_start()
def _draw_background(self, screen):
"""Рисует задний фон"""
# Заполним фон белым цветом.
screen.fill(Color.background)
def _draw_asteroids(self, screen):
"""Рисует все астероиды на сцене"""
for asteroid in self._scene.asteroids:
draw_with_duplicates(screen, self._screen_size,
asteroid, draw_asteroid)
def _draw_bullets(self, screen):
"""Рисует все пули на сцене"""
for bullet in self._scene.bullets:
draw_with_duplicates(screen, self._screen_size, bullet, draw_bullet)
def main_impl():
# Устанавливаем заголовок окна.
pygame.display.set_caption('Астероиды')
# Создаём таймер.
clock = pygame.time.Clock()
# Создаём окно для рисования.
screen = pygame.display.set_mode(GlobalConstants.screen_size)
# Создаёт объект-игру.
game = Game(GlobalConstants.screen_size)
# Инициализируем шаг обновления.
dt = 1.0 / GlobalConstants.max_fps
while True:
# Игра отрисовывает себя.
game.draw(screen)
# Результат рисования копируется на физический дисплей.
pygame.display.flip()
# Подготовка состояния игры для времени через dt.
if not game.update(dt):
# Выходим из игры.
break
# Устанавливаем шаг обновления в количество секунд,
# прошедших с предыдущего вызова clock.tick().
dt = clock.tick(GlobalConstants.max_fps) / 1000.0
def main():
# Инициализируем библиотеку PyGame.
pygame.init()
try:
main_impl()
finally:
# Всегда деинициализируем библиотеку PyGame, даже в случае падения.
pygame.quit()
if __name__ == '__main__':
main()