… или как убить время имея ipad и больше ничего...
Привет!
К сожалению, планшеты пока не заменяют компьютеры. Но покодить в поездке/полете это же жизненно необходимо. Поэтому я поискал какие ide есть под ipad, и собственно сегодня буду делать игрульку на Pythonista.
Простейшие программы, например кристаллики (да да, те самые, в которые вы играете в метро). Тетрис, змейка, fill — любой новичок, немного разобравшись, напишет их за 30 минут. Под катом — скриншоты, туториал, код.
Вот несколько скриншотов с того, что я наляпал:
Весь код будет приведен в конце
Сразу создадим сцену:
Ну и сразу запустим. У вас должен был получиться зеленый экран. Давайте сделаем какую-нибудь классную штуку, добавив в класс Game метод update (который сам вызывается системой), а в него изменение цвета фона.
Теперь у нас экран плавно меняется с желтого на белый и обратно.
Теперь создадим какой-нибудь объект. Создаем его также в методе setup:
Мы задали линию (mypath), создали по ней ShapeNode, указали ей цвет, а затем указали родителя (по сути одно и то же — указать родителя при создании, то есть ShapeNode(*..., parent=self) либо self.add_child(obj)).
Ну а в Game.update() мы меняем позицию объекта (tuple), причем оно в пикселях и считается от левого нижнего угла.
При каждом нажатии на экран мы создаем кружочек, место клика — touch.location.
Убираем весь написанный код, потому что хотим сделать более менее красиво (но, разумеется, делать будем велосипеды).
Для начала давайте создадим класс PhyObj:
Думаю, тут все тривиально. Внутри самого класса мы создали ноду, а также описали некоторые методы, которые будут делать наш код более читаемым.
PhyObj — это самый низкоуровневый объект в нашей игре, по сути — это абстрактный физический объект.
Теперь надо бы описать змейку, а так как она состоит из кусочков, опишем сначала один:
В конструкторе мы вызываем метод родителя класса и придаем себе форму и цвет. margin нужен чтобы квадратики не слипались и создавали некоторую сеточку.
Должно было получится:
А вот к примеру зачем нужен margin:
Отлично, теперь из этих кусочков надо склеить змею. Нам понадобится метод инициализации и метод move.
Для начала создадим инициализацию:
Давайте сразу попробуем ее нарисовать:
Ну и добавим метод move.
Сначала двигаем последнюю к предпоследней, потом предпоследнюю к предпредпоследней… потом вторую к первой. А первую на (x, y).
Собственно, змейка у нас двигается прям хорошо. Попробуем:
Если вы успели увидеть, то уползла она как надо. Дело в том, что update вызывается очень часто (и кстати необязательно с одинаковым интервалом), поэтому нам нужно считать, сколько времени прошло с последнего вызова и ждать, пока его «накопится» достаточно.
Короче, делаем:
time_gone возвращает True, если прошло больше времени, чем t. Теперь змейка будет передвигаться каждые 0.3 секунды. Получилось?
Теперь нужно сделать управление, то есть поворот во все четыре стороны:
А теперь надо сделать обработку touch_began, чтобы понять в какую область ткнул юзер. На самом деле это оказалось не так интересно, как я думал, поэтому тут можно просто скопировать:
Ну вот, попробуйте теперь поворачивать
Основной механизм проработан, осталось сделать проверку на столкновения и яблочки.
Начнем с проверки и добавим в змейку метод find_collisions
Теперь мы можем получить пару клеток, которые пересеклись. Хотелось бы покрасить их в красный, добавим метод die в Tile:
Добавим проверку в update и изменим setup:
Что получилось у меня:
Осталось сделать яблочки.
Сделаем удлинение змейки, причем чтобы оно выглядело красиво, новое звено будет добавлено в соответствии с последними двумя, то есть:
Добавим в змею методы:
find_dir находит направление, в которое направлен кончик хвоста нашей героини. append, несложно догадаться, добавляет ячейку. Добавим еще метод snake_lengthen в Game:
Последняя строка нужна чтобы змейка немножко подождала, а пользователь успел увидеть, что кусочек добавился.
Чтобы узнать пересекается ли что-то со змеей, добавим в нее метод intersect
Ура, остается только создать яблоко. Собственно, опишем яблоко за один заход:
Такой странный рандом нужен чтобы уместить наше яблочко по сетке. Тогда не нужно будет искать расстояние между мордой и яблоком и сравнивать его тыры-пыры. Просто на ифах. Пойдем в update и добавим в конец этой функции очень простые строки:
Ну вроде все, теперь змея удлиняется если попадает в яблоко и умирает, если стукается сама о себя.
Можно сделать звуковые эффекты:
Сделать плавное движение:
Иначе говоря, мы изменили логику position PhyObj. Раньше мы ориентировались на позицию графического элемента, а теперь есть отдельное поле логической позиции (то есть той, что используется для логики игры), и позиция графического элемента теперь свободна и может быть как-то изменена по-своему. А именно, используя Action, мы оставляем ей параллельный поток, где она и двигается.
Такая плавная змея получилась:
Ну и наконец label с длиной змеи:
Друзья, спасибо за внимание! Если что-то непонятно, спрашивайте. А если будет интересно — продолжу, еще есть что рассказать (но эта статейка и так длинновата).
Демонстрация работы кристалликов:
Привет!
О чем речь?
К сожалению, планшеты пока не заменяют компьютеры. Но покодить в поездке/полете это же жизненно необходимо. Поэтому я поискал какие ide есть под ipad, и собственно сегодня буду делать игрульку на Pythonista.
Что будем делать?
Простейшие программы, например кристаллики (да да, те самые, в которые вы играете в метро). Тетрис, змейка, fill — любой новичок, немного разобравшись, напишет их за 30 минут. Под катом — скриншоты, туториал, код.
Вот несколько скриншотов с того, что я наляпал:
Много скринов
Дисклеймер
Эта статья не только исключительно для новичков (но знающих python) и не позволит создавать world of tanks за десять минут и вообще какое-либо готовое приложение, но и автор не ручается за абсолютно красивый и правильный код с точки зрения религии программирования (хотя старается). А еще что-то стырено из примеров к pythonista и документации.
Весь код будет приведен в конце
Познакомимся с графикой в pythonista
Импорт
from scene import *
import random
Сразу создадим сцену:
class Game(Scene):
def setup(self):
self.background_color = "green"
run(Game(), LANDSCAPE)
Ну и сразу запустим. У вас должен был получиться зеленый экран. Давайте сделаем какую-нибудь классную штуку, добавив в класс Game метод update (который сам вызывается системой), а в него изменение цвета фона.
class Game(Scene):
# Ранее описанные методы
def update(self):
self.background_color = (1.0, 1.0, (math.sin(self.t) + 1) / 2)
Теперь у нас экран плавно меняется с желтого на белый и обратно.
Теперь создадим какой-нибудь объект. Создаем его также в методе setup:
class Game(Scene):
def setup(self):
self.background_color = "white"
mypath = ui.Path.rect(0, 0, 50, 50)
self.obj = ShapeNode(mypath)
self.obj.color = "purple" #А еще можно указывать как в html, например #FF00FF. Или в tuple, то есть (1.0, 0.0, 1.0). А еще в конец можно приписать alpha, то есть прозрачность
self.add_child(self.obj)
def update(self):
self.obj.position = (500 + 200 * math.sin(self.t), 500 + 200 * math.cos(self.t))
Мы задали линию (mypath), создали по ней ShapeNode, указали ей цвет, а затем указали родителя (по сути одно и то же — указать родителя при создании, то есть ShapeNode(*..., parent=self) либо self.add_child(obj)).
Ну а в Game.update() мы меняем позицию объекта (tuple), причем оно в пикселях и считается от левого нижнего угла.
Заметьте, нам не нужно перерисовывать сцену (хотя можно). Объекты и ноды, родитель которого — сцена (или какой-то ее дочерний объект) перерисовываются самиПоследнее, что мы пройдем в этом разделе — touch_began (а также touch_moved и touch_ended). Несложно догадаться, метод ловит нажатия на экран. Давайте опробуем:
class Game(Scene):
def touch_began(self, touch):
mypath = ui.Path.oval(0, 0, 50, 50)
obj = ShapeNode(mypath)
obj.color = (0.5, 0.5, 1.0)
self.add_child(obj)
obj.position = touch.location
При каждом нажатии на экран мы создаем кружочек, место клика — touch.location.
Готовы писать змейку
Механизм игры
Змейка — массив квадратиков, когда двигается голова — второй кусочек пододвигается на место первого, третий — на место второго и т. д. Чтобы узнать пересечение головы с хвостом, мы будем сравнивать ее с каждым кусочком хвоста
Убираем весь написанный код, потому что хотим сделать более менее красиво (но, разумеется, делать будем велосипеды).
Для начала давайте создадим класс PhyObj:
class PhyObj:
def __init__(self, path, color, parent):
self.graph_obj = ShapeNode(path, parent=parent)
self.parent = parent
self.graph_obj.color = color
def setpos(self, x, y):
self.graph_obj.position = (x, y)
def getpos(self):
return self.graph_obj.position
def move(self, x, y):
self.graph_obj.position += (x, y)
Думаю, тут все тривиально. Внутри самого класса мы создали ноду, а также описали некоторые методы, которые будут делать наш код более читаемым.
Холивары
Лично я предпочитаю сначала создавать низкоуровневые классы, у которых пусть будет много дублирующихся методов и свойств, зато потом уже код в высокоуровневых становится очень красивым и читаемым.
PhyObj — это самый низкоуровневый объект в нашей игре, по сути — это абстрактный физический объект.
Описание змейки
Теперь надо бы описать змейку, а так как она состоит из кусочков, опишем сначала один:
class Tile(PhyObj):
def __init__(self, parent, size, margin=4):
super().__init__(ui.Path.rect(0, 0, size[0] - margin, size[1] - margin), "#66FF66", parent)
def die(self):
self.graph_obj.color = "red"
В конструкторе мы вызываем метод родителя класса и придаем себе форму и цвет. margin нужен чтобы квадратики не слипались и создавали некоторую сеточку.
class Game(Scene):
def setup(self):
self.tile = Tile(self, (40, 40))
self.tile.setpos(100, 100)
Должно было получится:
А вот к примеру зачем нужен margin:
class Game(Scene):
def setup(self):
tile1 = Tile(self, (40, 40))
tile1.setpos(100, 100)
tile2 = Tile(self, (40, 40))
tile2.setpos(140, 100)
Отлично, теперь из этих кусочков надо склеить змею. Нам понадобится метод инициализации и метод move.
Для начала создадим инициализацию:
class Snake:
def __init__(self, length, width, initpos, parent):
self.width = width # Это ширина каждой клетки
self.tiles = [Tile(parent, (width, width)) for i in range(length)] # Здесь мы создаем массив из наших клеток
for i, tile in enumerate(self.tiles):
tile.setpos(initpos[0] + i * self.width, initpos[1]) #Ну а здесь мы ставим позицию каждой клетке
Давайте сразу попробуем ее нарисовать:
class Game(Scene):
def setup(self):
self.snake = Snake(10, 40, (200, 200), self)
Ну и добавим метод move.
class Snake:
def move(self, x, y):
for i in range(len(self.tiles) - 1, 0, -1):
self.tiles[i].setpos(*self.tiles[i - 1].getpos())
self.tiles[0].move(x * self.width, y * self.width)
Сначала двигаем последнюю к предпоследней, потом предпоследнюю к предпредпоследней… потом вторую к первой. А первую на (x, y).
Собственно, змейка у нас двигается прям хорошо. Попробуем:
class Game(self):
# <...>
def update(self):
self.snake.move(0, 1)
Если вы успели увидеть, то уползла она как надо. Дело в том, что update вызывается очень часто (и кстати необязательно с одинаковым интервалом), поэтому нам нужно считать, сколько времени прошло с последнего вызова и ждать, пока его «накопится» достаточно.
Короче, делаем:
class Game(Scene):
def time_reset(self):
self.last_time = self.t
def time_gone(self, t):
if self.t - self.last_time > t:
res = True
self.time_reset()
else:
res = False
return res
def setup(self):
self.snake = Snake(10, 40, (200, 200), self)
self.time_reset()
def update(self):
if self.time_gone(0.3):
self.snake.move(0, 1)
time_gone возвращает True, если прошло больше времени, чем t. Теперь змейка будет передвигаться каждые 0.3 секунды. Получилось?
Управление
Теперь нужно сделать управление, то есть поворот во все четыре стороны:
class Game(Scene):
# <...>
def setup(self):
# <...>
self.dir = (0, 1) # это направление по умолчанию
def update(self):
if self.time_gone(0.3):
self.snake.move(*self.dir)
А теперь надо сделать обработку touch_began, чтобы понять в какую область ткнул юзер. На самом деле это оказалось не так интересно, как я думал, поэтому тут можно просто скопировать:
class Game(Scene):
# <...>
def touch_began(self, touch):
ws = touch.location[0] / self.size.w
hs = touch.location[1] / self.size.h
aws = 1 - ws
if ws > hs and aws > hs:
self.dir = (0, -1)
elif ws > hs and aws <= hs:
self.dir = (1, 0)
elif ws <= hs and aws > hs:
self.dir = (-1, 0)
else:
self.dir = (0, 1)
Ну вот, попробуйте теперь поворачивать
Основной механизм проработан, осталось сделать проверку на столкновения и яблочки.
Столкновение с хвостом
Начнем с проверки и добавим в змейку метод find_collisions
class Snake:
# <...>
def find_collisions(self):
for i in range(1, len(self.tiles)):
if self.tiles[i].getpos() == self.tiles[0].getpos():
return self.tiles[i], self.tiles[0]
return False
Теперь мы можем получить пару клеток, которые пересеклись. Хотелось бы покрасить их в красный, добавим метод die в Tile:
class Tile(PhyObj):
# <...>
def die(self):
self.graph_obj.color = "red"
Добавим проверку в update и изменим setup:
class Game(Scene):
# <...>
def setup(self):
self.snake = Snake(30, 40, (200, 200), self) # Создаем змейку
self.time_reset()
self.dir = (0, 1)
self.game_on = True #А запущена ли игра?
def update(self):
if not self.game_on: #Если не запущена, выходим
return
col = self.snake.find_collisions() #Есть ли коллизии?
if col:
for tile in col:
tile.die() # Красим все в красный
self.game_on = False # Останавливаем игру
if self.time_gone(0.3):
self.snake.move(*self.dir)
Что получилось у меня:
Осталось сделать яблочки.
Удлинение и яблочки
Сделаем удлинение змейки, причем чтобы оно выглядело красиво, новое звено будет добавлено в соответствии с последними двумя, то есть:
Добавим в змею методы:
class Snake:
# <...>
def find_dir(self, x1, y1, x2, y2):
if x1 == x2 and y1 > y2:
return (0, 1)
elif x1 == x2 and y1 < y2:
return (0, -1)
elif y1 == y2 and x1 > x2:
return (1, 0)
elif y1 == y2 and x1 < x2:
return (-1, 0)
else:
assert False, "Error!"
def append(self):
if len(self.tiles) > 1:
lastdir = self.find_dir(*self.tiles[-1].getpos(), *self.tiles[-2].getpos())
else:
lastdir = (-self.parent.dir[0], -self.parent.dir[1])
self.tiles.append(Tile(self.parent, (self.width, self.width)))
x_prev, y_prev = self.tiles[-2].getpos()
self.tiles[-1].setpos(x_prev + lastdir[0] * self.width, y_prev + lastdir[1] * self.width)
find_dir находит направление, в которое направлен кончик хвоста нашей героини. append, несложно догадаться, добавляет ячейку. Добавим еще метод snake_lengthen в Game:
class Game(Scene):
# <...>
def snake_lengthen(self):
self.snake.append()
self.time_reset()
Последняя строка нужна чтобы змейка немножко подождала, а пользователь успел увидеть, что кусочек добавился.
Чтобы узнать пересекается ли что-то со змеей, добавим в нее метод intersect
class Snake:
# <...>
def getpos(self):
return self.tiles[0].getpos()
def intersect(self, x, y):
return self.getpos() == (x, y)
Ура, остается только создать яблоко. Собственно, опишем яблоко за один заход:
class Apple(PhyObj):
def __init__(self, width, size, parent):
super().__init__(ui.Path.oval(0, 0, size[0], size[1]), "#55AAFF", parent)
self.parent = parent
self.width = width
self.dislocate()
def dislocate(self):
a = random.randint(2, int(self.parent.size.w / self.width) - 2)
b = random.randint(2, int(self.parent.size.h / self.width) - 2)
self.setpos(a * self.width, b * self.width)
Такой странный рандом нужен чтобы уместить наше яблочко по сетке. Тогда не нужно будет искать расстояние между мордой и яблоком и сравнивать его тыры-пыры. Просто на ифах. Пойдем в update и добавим в конец этой функции очень простые строки:
class Game(Scene):
# <...>
def setup(self):
self.apple = Apple(40, (50, 50), self) #Число 40 - ширина сетки, должно совпадать с вторым аргументом в Snake()
# <...>
def update(self):
# <...>
if self.snake.intersect(*self.apple.getpos()):
self.snake_lengthen()
self.apple.dislocate()
Ну вроде все, теперь змея удлиняется если попадает в яблоко и умирает, если стукается сама о себя.
Бонус
Можно сделать звуковые эффекты:
import sound
class Game(Scene):
# <...>
def snake_lengthen(self):
self.snake.append()
self.time_reset()
sound.play_effect('arcade:Powerup_1', 0.25, 0.8)
Сделать плавное движение:
class Game(Scene):
# <...>
def setup(self):
self.game_on = False
self.GLOBAL_TIMING = 0.2
self.GLOBAL_WIDTH = 40
self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
self.snake = Snake(10, self.GLOBAL_WIDTH, (200, 200), self)
self.time_reset()
self.dir = (0, 1)
self.game_on = True
class Snake:
# <...>
def move(self, x, y):
for i in range(len(self.tiles) - 1, 0, -1):
self.tiles[i].setpos(*self.tiles[i - 1].getpos(), self.parent.GLOBAL_TIMING)
self.tiles[0].move(x * self.width, y * self.width, self.parent.GLOBAL_TIMING)
class PhyObj:
def __init__(self, path, color, parent):
self.graph_obj = ShapeNode(path, parent=parent)
self.parent = parent
self.graph_obj.color = color
self.pos = self.graph_obj.position
def setpos(self, x, y, t=0.0):
self.pos = (x, y)
self.graph_obj.run_action(Action.move_to(x, y, t)) # Плавное движение к x, y в течении времени t
def getpos(self):
return self.pos
def move(self, x, y, t=0.0):
self.pos = (self.pos[0] + x, self.pos[1] + y)
self.graph_obj.run_action(Action.move_by(x, y, t))
Иначе говоря, мы изменили логику position PhyObj. Раньше мы ориентировались на позицию графического элемента, а теперь есть отдельное поле логической позиции (то есть той, что используется для логики игры), и позиция графического элемента теперь свободна и может быть как-то изменена по-своему. А именно, используя Action, мы оставляем ей параллельный поток, где она и двигается.
Такая плавная змея получилась:
Ну и наконец label с длиной змеи:
class Game(Scene):
# <...>
def setup(self):
self.game_on = False
self.GLOBAL_TIMING = 0.2
self.GLOBAL_WIDTH = 40
self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
self.snake = Snake(30, self.GLOBAL_WIDTH, (200, 200), self)
self.time_reset()
self.dir = (0, 1)
self.label = LabelNode("", font=("Chalkduster", 20), parent=self, position=(self.size.w / 2, self.size.h - 100)) #Размещаем ее по центру
self.update_labels()
self.game_on = True
def update_labels(self):
self.label.text = "Length: " + str(len(self.snake.tiles))
def update(self):
if not self.game_on:
return
col = self.snake.find_collisions()
if col:
for tile in col:
tile.die()
self.game_on = False
if self.time_gone(self.GLOBAL_TIMING):
self.snake.move(*self.dir)
if self.snake.intersect(*self.apple.getpos()):
self.snake_lengthen()
self.apple.dislocate()
self.update_labels() #Обновляем тут
Друзья, спасибо за внимание! Если что-то непонятно, спрашивайте. А если будет интересно — продолжу, еще есть что рассказать (но эта статейка и так длинновата).
Весь код змейки
from scene import *
import random
import math
import sound
class PhyObj:
def __init__(self, path, color, parent):
self.graph_obj = ShapeNode(path, parent=parent)
self.parent = parent
self.graph_obj.color = color
self.pos = self.graph_obj.position
def setpos(self, x, y, t=0.0):
self.pos = (x, y)
self.graph_obj.run_action(Action.move_to(x, y, t))
def getpos(self):
return self.pos
def move(self, x, y, t=0.0):
self.pos = (self.pos[0] + x, self.pos[1] + y)
self.graph_obj.run_action(Action.move_by(x, y, t))
class Tile(PhyObj):
def __init__(self, parent, size, margin=4):
super().__init__(ui.Path.rect(0, 0, size[0] - margin, size[1] - margin), "#66FF66", parent)
def die(self):
self.graph_obj.color = "red"
class Snake:
def __init__(self, length, width, initpos, parent):
self.width = width
self.tiles = [Tile(parent, (width, width)) for i in range(length)]
for i, tile in enumerate(self.tiles):
tile.setpos(initpos[0] + i * self.width, initpos[1])
self.parent = parent
def move(self, x, y):
for i in range(len(self.tiles) - 1, 0, -1):
self.tiles[i].setpos(*self.tiles[i - 1].getpos(), self.parent.GLOBAL_TIMING)
self.tiles[0].move(x * self.width, y * self.width, self.parent.GLOBAL_TIMING)
def find_collisions(self):
for i in range(1, len(self.tiles)):
if self.tiles[i].getpos() == self.tiles[0].getpos():
return self.tiles[i], self.tiles[0]
return False
def find_dir(self, x1, y1, x2, y2):
if x1 == x2 and y1 > y2:
return (0, 1)
elif x1 == x2 and y1 < y2:
return (0, -1)
elif y1 == y2 and x1 > x2:
return (1, 0)
elif y1 == y2 and x1 < x2:
return (-1, 0)
else:
assert False, "Error!"
def append(self):
if len(self.tiles) > 1:
lastdir = self.find_dir(*self.tiles[-1].getpos(), *self.tiles[-2].getpos())
else:
lastdir = (-self.parent.dir[0], -self.parent.dir[1])
self.tiles.append(Tile(self.parent, (self.width, self.width)))
x_prev, y_prev = self.tiles[-2].getpos()
self.tiles[-1].setpos(x_prev + lastdir[0] * self.width, y_prev + lastdir[1] * self.width)
def getpos(self):
return self.tiles[0].getpos()
def intersect(self, x, y):
return self.getpos() == (x, y)
class Apple(PhyObj):
def __init__(self, width, size, parent):
super().__init__(ui.Path.oval(0, 0, size[0], size[1]), "#55AAFF", parent)
self.parent = parent
self.width = width
self.dislocate()
def dislocate(self):
a = random.randint(2, int(self.parent.size.w / self.width) - 2)
b = random.randint(2, int(self.parent.size.h / self.width) - 2)
self.setpos(a * self.width, b * self.width)
class Game(Scene):
def snake_lengthen(self):
self.snake.append()
self.time_reset()
sound.play_effect('arcade:Powerup_1', 0.25, 0.8)
def time_reset(self):
self.last_time = self.t
def time_gone(self, t):
if self.t - self.last_time > t:
res = True
self.time_reset()
else:
res = False
return res
def setup(self):
self.game_on = False
self.GLOBAL_TIMING = 0.2
self.GLOBAL_WIDTH = 40
self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
self.snake = Snake(30, self.GLOBAL_WIDTH, (200, 200), self)
self.time_reset()
self.dir = (0, 1)
self.label = LabelNode("", font=("Chalkduster", 20), parent=self, position=(self.size.w / 2, self.size.h - 100))
self.update_labels()
self.game_on = True
def update_labels(self):
self.label.text = "Length: " + str(len(self.snake.tiles))
def update(self):
if not self.game_on:
return
col = self.snake.find_collisions()
if col:
for tile in col:
tile.die()
self.game_on = False
if self.time_gone(self.GLOBAL_TIMING):
self.snake.move(*self.dir)
if self.snake.intersect(*self.apple.getpos()):
self.snake_lengthen()
self.apple.dislocate()
self.update_labels()
def touch_began(self, touch):
ws = touch.location[0] / self.size.w
hs = touch.location[1] / self.size.h
aws = 1 - ws
if ws > hs and aws > hs:
self.dir = (0, -1)
elif ws > hs and aws <= hs:
self.dir = (1, 0)
elif ws <= hs and aws > hs:
self.dir = (-1, 0)
else:
self.dir = (0, 1)
run(Game(), LANDSCAPE)
Код кристалликов WhiteBlackGoose edition
Забавно, что после того, как я ее сделал, я обнаружил что-то ОЧЕНЬ похожее в примерах из самой pythonista. Но у меня чуть больше фич :)
from scene import *
from math import pi
from random import uniform as rnd, choice, randint
import sys
import random
A = Action
sys.setrecursionlimit(1000000)
colors = ['pzl:Green5', "pzl:Red5", "pzl:Blue5"] + ["pzl:Purple5", "pzl:Button2"] + ["plf:Item_CoinGold"]
global inited
inited = False
class Explosion (Node):
def __init__(self, brick, *args, **kwargs):
Node.__init__(self, *args, **kwargs)
self.position = brick.position
for dx, dy in ((-1, -1), (1, -1), (-1, 1), (1, 1)):
p = SpriteNode(brick.texture, scale=0.5, parent=self)
p.position = brick.size.w/4 * dx, brick.size.h/4 * dy
p.size = brick.size
d = 0.6
r = 30
p.run_action(A.move_to(rnd(-r, r), rnd(-r, r), d))
p.run_action(A.scale_to(0, d))
p.run_action(A.rotate_to(rnd(-pi/2, pi/2), d))
self.run_action(A.sequence(A.wait(d), A.remove()))
class Brick (SpriteNode):
def __init__(self, brick_type, *args, **kwargs):
img = colors[brick_type]
SpriteNode.__init__(self, img, *args, **kwargs)
self.brick_type = brick_type
self.is_on = True
self.lf = True
self.enabled = True
def destroy(self):
self.remove_from_parent()
self.is_on = False
def mark(self):
self.lf = False
def demark(self):
self.lf = True
class Game(Scene):
def brickgetpos(self, i, j):
return (self.Woff + j * self.W, self.Hoff + i * self.H)
def brick(self, ty, i, j):
b = Brick(ty, size=(self.W, self.H), position=self.brickgetpos(i, j), parent=self.game_node)
b.rotation = random.random()
return b
def random_brick_type(self):
if random.random() < 0.992:
return random.randint(0, 3)
else:
if random.random() < 0.8:
return 5
else:
return 4
def setup(self):
FONT = ('Chalkduster', 20)
self.score_label = LabelNode('Score: 0', font=FONT, position=(self.size.w/2-100, self.size.h-40), parent=self)
self.score = 0
self.last_score_label = LabelNode('Delta: +0', font=FONT, position=(self.size.w/2-300, self.size.h-40), parent=self)
self.last_score = 0
#self.avg_label = LabelNode('Speed: +0/s', font=FONT, position=(self.size.w/2+100, self.size.h-40), parent=self)
#self.max_label = LabelNode('Peak: +0/s', font=FONT, position=(self.size.w/2+300, self.size.h-40), parent=self)
#self.max_speed = 0
self.game_time = 120
self.timel = LabelNode('Time: ' + str(self.game_time) + "s", font=FONT, position=(self.size.w/2+300, self.size.h-40), parent=self)
self.gems = [0 for i in colors]
self.effect_node = EffectNode(parent=self)
self.game_node = Node(parent=self.effect_node)
self.l = [0 for i in colors]
self.lt = [0 for i in colors]
for i in range(len(colors)):
R = 50 if i == 6 else 35
self.l[i] = Brick(i, size=(R, R), position=(40, self.size.h-100-i*40), parent=self.game_node)
self.lt[i] = LabelNode(": 0", font=FONT, position=(self.l[i].position[0] + 40, self.l[i].position[1]), parent=self)
self.WB = 30
self.HB = 30
self.W = 900 // self.WB
self.H = 900 // self.HB
self.colcount = 4
self.Woff = (int(self.size.w) - self.W * self.WB + self.W) // 2
self.Hoff = self.H + 10
self.net = [[self.brick(self.random_brick_type(), i, j) for i in range(self.HB)] for j in range(self.WB)]
#self.touch_moved = self.touch_began
self.start_time = self.t
self.game_on = True
global inited
inited = True
def demark(self):
for bricks in self.net:
for brick in bricks:
brick.demark()
def howfar(self, x, y):
alt = 0
for i in range(y):
if not self.net[x][i].is_on:
alt += 1
return alt
def update(self):
global inited
if not inited:
return
self.game_on = self.t - self.start_time < self.game_time
if self.game_on:
self.timel.text = "Time: " + str(round(self.game_time - (self.t - self.start_time))) + "s"
else:
self.timel.text = "Game over"
#if speed > self.max_speed:
# self.max_speed = speed
# self.max_label.text = "Peak: +" + str(round(self.max_speed)) + "/s"
def gravity(self, x, y):
alt = self.howfar(x, y)
if alt == 0:
return
self.net[x][y].destroy()
self.net[x][y - alt] = self.brick(self.net[x][y].brick_type, y, x)
self.net[x][y - alt].position = self.net[x][y].position
self.net[x][y - alt].rotation = self.net[x][y].rotation
self.net[x][y - alt].enabled = False
self.net[x][y - alt].run_action(A.sequence(A.move_to(*self.brickgetpos(y - alt, x), 0.2 * alt ** 0.5, TIMING_EASE_IN_2), A.call(lambda: self.enable_cell(x, y - alt))))
def enable_cell(self, x, y):
self.net[x][y].enabled = True
def fall(self):
for x in range(self.WB):
for y in range(self.HB):
if self.net[x][y].is_on:
self.gravity(x, y)
def update_scores(self):
self.score += self.last_score
self.score_label.text = "Score: " + str(self.score)
self.last_score_label.text = "Delta: +" + str(self.last_score)
self.last_score = 0
def update_cells(self):
for i in range(self.WB):
for j in range(self.HB):
if not self.net[i][j].is_on:
self.net[i][j] = self.brick(self.random_brick_type(), j + self.HB, i)
self.net[i][j].enabled = True
self.net[i][j].run_action(A.sequence(A.move_to(*self.brickgetpos(j, i), 0.2 * self.HB ** 0.5, TIMING_EASE_IN_2), A.call(lambda: self.enable_cell(i, j))))
def inbounds(self, x, y):
return (x >= 0) and (y >= 0) and (x < self.WB) and (y < self.HB)
def bomb(self, x, y, radius):
score = 0
bc = 0
for i in range(round(4 * radius ** 2)):
rad = random.random() * radius
ang = random.random() * 2 * pi
xp, yp = x + sin(ang) * rad, y + cos(ang) * rad
xp, yp = int(xp), int(yp)
if self.inbounds(xp, yp):
score += self.explode(xp, yp)
self.fall()
self.give_score(round(score / 1.7), self.brickgetpos(y, x))
def laser(self, x, y):
score = 0
coords = []
for i in range(self.HB):
for j in range(-1, 1 + 1, 1):
coords.append((x + j, i))
for i in range(self.WB):
coords.append((i, y))
for i in range(-self.HB, self.HB):
coords.append((x + i, y + i))
for i in range(-self.WB, self.WB):
coords.append((x - i, y + i))
bc = 0
for x, y in coords:
if not self.inbounds(x, y):
continue
score += self.explode(x, y)
self.fall()
self.give_score(score, self.brickgetpos(y, x))
def getty(self, x, y):
if not self.inbounds(x, y) or not self.net[x][y].is_on:
return -1
else:
return self.net[x][y].brick_type
def popupt(self, text, position_, font_=("Arial", 30), color_="white"):
label = LabelNode(text, font=font_, color=color_, parent=self, position=position_)
label.run_action(A.sequence(A.wait(1), A.call(label.remove_from_parent)))
def give_score(self, count, xy):
self.last_score = int(count ** 2.5)
size = 10
if self.last_score > 50000:
size = 60
elif self.last_score > 20000:
size = 40
elif self.last_score > 10000:
size = 30
elif self.last_score > 5000:
size = 25
elif self.last_score > 2000:
size = 20
elif self.last_score > 1000:
size = 15
if self.last_score > 0:
self.popupt("+" + str(self.last_score), xy, font_=("Chalkduster", int(size * 1.5)))
self.update_scores()
def touch_began(self, touch):
if not self.game_on:
return
x, y = touch.location
x, y = x + self.W / 2, y + self.H / 2
W, H = get_screen_size()
x, y = x, y
x, y = int(x), int(y)
x, y = x - self.Woff, y - self.Hoff
x, y = x // self.W, y // self.H
if not self.inbounds(x, y):
return
count = self.react(self.net[x][y].brick_type, x, y, True)
self.demark()
if self.getty(x, y) in [0, 1, 2, 3]:
if count >= 2:
self.react(self.net[x][y].brick_type, x, y)
self.fall()
self.give_score(count, touch.location)
elif self.getty(x, y) == 4:
self.bomb(x, y, 5 * count)
elif self.getty(x, y) == 5:
self.explode(x, y)
self.fall()
self.update_cells()
def explode(self, x, y):
if self.net[x][y].is_on:
self.net[x][y].destroy()
self.gems[self.net[x][y].brick_type] += 1
s = str(self.gems[self.net[x][y].brick_type])
self.lt[self.net[x][y].brick_type].text = " " * len(s) + ": " + s
self.game_node.add_child(Explosion(self.net[x][y]))
return True
else:
return False
def react(self, col, x, y, ignore=False):
if self.inbounds(x, y) and self.net[x][y].brick_type == col and self.net[x][y].is_on and self.net[x][y].lf and self.net[x][y].enabled:
if not ignore:
self.explode(x, y)
else:
self.net[x][y].mark()
r = 1
r += self.react(col, x + 1, y + 0, ignore)
r += self.react(col, x - 1, y - 0, ignore)
r += self.react(col, x + 0, y + 1, ignore)
r += self.react(col, x - 0, y - 1, ignore)
return r
else:
return 0
def destroy_brick(self, x, y):
self.net[x][y].destroy()
run(Game(), LANDSCAPE, show_fps=True)
Демонстрация работы кристалликов:
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Кто ты, читатель?
0% Я специалист в pythonista, мне не понравилось0
5.71% Я специалист в pythonista, мне понравилось2
71.43% Я новичок в pythonista, было более менее понятно25
22.86% Я новичок в pythonista, совсем непонятно8
Проголосовали 35 пользователей. Воздержались 14 пользователей.