Вдохновившись статьёй, посвящённой написанию клеточного автомата на Godot и экспорту проекта в HTML, хочу показать вам, как использовать для этих целей модерновый онлайн-движок p5py
. Код живой не только потому, что мы про игру «Жизнь», но и благодаря способу его разработки и запуска. Всё очень живо!
Чёрный плащ
TL;DR: финальный проект вот здесь. Только кликните, и он появится.
В чем магия?
Мы получим похожий результат, но намного быстрее и увлекательнее. Нам не придётся ничего настраивать и скачивать, что отлично подходит для новичков. Просто переходите по ссылкам в статье и запускайте рабочие примеры.
Экспорт в HTML нам тоже не понадобится, поскольку код на Python, благодаря
p5py
и онлайн-IDE, запускается прямо в браузере.Более того, если у вас возникнет творческая идея, как улучшить код, и вы её реализуете, то: а) сразу увидите результат, б) нажав «Сохранить», получите готовую ссылку, которой можно поделиться с друзьями или в комментариях.
p5py
Это адаптация популярного Processing (p5.js) для Python. Я написал его для проведения занятий в детских кружках по программированию и для книги, про которую уже рассказывал в статье «Как я написал книгу для детей: «Мама, не отвлекай. Я Python учу!».
Для него же разработана и онлайн-IDE, которая запускается по всем ссылкам в этой статье. Там всего две кнопки: «Запустить» и «Сохранить». Не перепутаете.
Получаем
Вместо GDScript — самый стандартный Python (+модуль
p5py
).Не нужен отдельный экспорт — результат сразу доступен по ссылке онлайн.
Живая песочница — код автоматически перезапускается в живом режиме по мере его написания (по желанию можно отключить).
Размер не гигабайт, а 4,5 мегабайта.
А был он в Жизнь влюбленный...
Воссоздадим «Игру Жизнь» Конвея — клеточный автомат, способный генерировать сложные паттерны, имитирующие жизнь.
Исходный код на Godot:
extends Node2D
@export var cell_scene : PackedScene
var row_count : int = 45
var column_count : int = 80
var cell_width: int = 15
var cell_matrix: Array = []
var previous_cell_states: Array = []
var is_game_running: bool = false
var is_interactive_mode: bool = false
Наш код на p5py
:
row_count = 45
column_count = 80
cell_width = 15
Почему такой короткий? А потому, потому, потому...
...что мы используем проектно-ориентированный подход и не пишем строчки кода, которые не пригодятся на следующем шаге. Зачем их держать в голове? Только шум создают. А пока что, всё, что нам нужно знать, — это размер поля 45х80
и размер ячейки 15х15
.
Полем, полем, полем...
Белым, белым сделаем пустое поле из клеточек.
from p5py import *
run()
row_count = 45
column_count = 80
cell_width = 15
size(column_count * cell_width, row_count * cell_width)
def draw():
background(255) # Белый фон
draw_grid()
def draw_grid():
stroke(0) # Черные линии сетки
for x in range(column_count + 1):
line(x * cell_width, 0, x * cell_width, row_count * cell_width)
for y in range(row_count + 1):
line(0, y * cell_width, column_count * cell_width, y * cell_width)
# Здесь мы просто отображаем пустое поле
Нажмите здесь, чтобы запустить. Если вдруг выдало ошибку, напишите, пожалуйста, в личку. Версия всё ещё 0.XXX, могут быть баги.
Поэкспериментируйте! Поменяйте цвет фона и линий. Например,
background(200, 100, 0)
— это оттенок оранжевого, аstroke(255, 120, 0)
— оттенок красного.
Случай решит, как заполнить ячейки
Улучшим оригинальный код:
Сразу заполняем поле случайными значениями — так игроку интереснее наблюдать за процессом. При запуске сразу что-то происходит.
А так как в online-IDE
p5py
легко нажать крестик, закрыв программу, и сразу же нажатьRUN
, снова её запустив, — можно играть с разными стартовыми условиями.
# Инициализация состояния клеток случайными значениями
cell_matrix = [[rand(0, 15) <= 1 for _ in range(row_count)] for _ in range(column_count)]
def draw_cells():
for col in range(column_count):
for row in range(row_count):
if cell_matrix[col][row]:
fill(0) # Черный цвет для живых клеток
else:
no_fill() # Без заливки для мертвых клеток
rect(col * cell_width, row * cell_width, cell_width, cell_width)
Вот так получилось. Здесь я сделал ячейки жёлтого цвета. Можете поэкспериментировать с кодом вживую, например заменив число 15 на другое, чтобы изменить плотность заполнения поля.
Подготовку закончили, теперь у нас есть и сетка, и клетки, случайным образом раскрашенные.
Самое время перейти к...
Обновлению поля. Игра не по правилам
Добавим функцию update_game_state()
, которая просто пройдёт по каждой клетке поля и запишет в неё новый статус. Который узнает, в свою очередь, у функции get_next_state()
.
def update_game_state():
global cell_matrix
for col in range(column_count):
for row in range(row_count):
cell_matrix[col][row] = get_next_state(col, row)
def get_next_state(column, row):
return rand(0, 15) <= 1
А вот и ссылка на поиграться.
Да-да, пока игра не по правилам. Мы используем mock (заглушку), чтобы побыстрее увидеть результат и было интересно по шагам улучшать программу.
А теперь перейдем к...
Игровым правилам
А вот и самое важное — создание правил для игры «Жизнь». Пишем функции для подсчёта живых соседей и определения будущего состояния каждой клетки. Это классические правила Конвея: клетка становится живой, если у неё ровно три живых соседних клетки, и остаётся живой, если у неё две или три живых соседних клетки.
Исходный код на Godot:
func get_count_of_alive_neighbours(column, row):
var count = 0
for x in range(-1, 2):
for y in range(-1, 2):
if not (x == 0 and y == 0):
var neighbor_column = column + x
var neighbor_row = row + y
if neighbor_column >= 0 and neighbor_column < column_count and neighbor_row >= 0 and neighbor_row < row_count:
if previous_cell_states[neighbor_column][neighbor_row]:
count += 1
return count
func get_next_state(column, row):
var current = previous_cell_states[column][row]
var neighbours_alive = get_count_of_alive_neighbours(column, row)
if current:
return neighbours_alive == 2 or neighbours_alive == 3
else:
return neighbours_alive == 3
Практически такой же на p5py
:
def get_next_state(column, row):
alive_neighbors = count_alive_neighbors(column, row)
current = previous_cell_states[column][row]
if current:
# Cell is alive, it stays alive if it has 2 or 3 neighbors
return alive_neighbors == 2 or alive_neighbors == 3
else:
# Cell is dead, it becomes alive if it has exactly 3 neighbors
return alive_neighbors == 3
def count_alive_neighbors(column, row):
count = 0
for x in range(-1, 2):
for y in range(-1, 2):
if x == 0 and y == 0:
continue # Skip the cell itself
neighbor_col = column + x
neighbor_row = row + y
if 0 <= neighbor_col < column_count and 0 <= neighbor_row < row_count:
if previous_cell_states[neighbor_col][neighbor_row]:
count += 1
return count
Но и функцию update_game_state()
нам тоже придётся немного поменять, чтобы сохранять предыдущее значение ячеек во временный массив.
Как думаете, для чего этот шаг?
def update_game_state():
global previous_cell_states, cell_matrix
# Copy current state to previous
for col in range(column_count):
for row in range(row_count):
previous_cell_states[col][row] = cell_matrix[col][row]
# Apply the Game of Life rules
for col in range(column_count):
for row in range(row_count):
cell_matrix[col][row] = get_next_state(col, row)
А если хотите больше Python-стиля, то замените на previous_cell_states = [row.copy() for row in cell_matrix]
.
Парам-пам-пам
Ну вот и всё! Вот он, наш готовый код.
NoDB: баг или фича
Вы уже заметили, что ссылка на наш код длинная, как предложения в книгах Хулио Кортасара. Но так задумано. Это современный подход NoDB (точнее, URL-based storage), когда небольшие программы немного сжимаются и сохраняются прямо в URL. Небольшие, поручик, я сказал, небольшие!
Вообще, за несколько часов перед публикацией предыдущей статьи сломалась основная база данных, и я временно посадил проект на запасной аэродром NoDB, который был готов раньше. Как появится время, верну сохранение в постоянной базе данных и короткие ссылки.
Добавим интерактивность
В оригинальной статье пользователь мог сам рисовать существ на поле. Давайте сделаем так же на p5py
, но только немного улучшим использование исходного кода:
Сразу включим режим «вмешательства». Пусть игрок сможет сразу добавлять новые фигуры:
is_game_running = True
.Уберём кнопку включения-выключения этого рисования, так как неясно, зачем она нужна. Только интерфейс перегружает.
Заменим кнопку остановки игры на... автоматическое действие. При нажатии мышки игрок может нарисовать новую фигуру, а игра на это время приостанавливается. Как только игрок отпустит мышку — игра возобновляется. Да, здесь я осознанно жертвую возможностью в паузе нарисовать много фигур сразу, но у нас же демо, а для демо так будет интуитивно понятнее.
Добавим в def draw():
...подсказку:
fill(255, 140)
text_size(20)
text("Вы можете нарисовать фигуру мышкой", 30, 30)
…и запуск/остановку игры:
if is_game_running:
update_game_state()
if mouse_is_pressed:
toggle_cell_at_mouse_position()
is_game_running = False
def toggle_cell_at_mouse_position():
col = int(mouse_x / cell_width)
row = int(mouse_y / cell_width)
if 0 <= col < column_count and 0 <= row < row_count:
cell_matrix[col][row] = True
def mouse_released():
global is_game_running
is_game_running = True
The end. Рисуйте...
Когда мобайл-друзья со мной
Если вы вдруг читаете эту статью на мобильном и кликнули по одной из ссылок выше, то результат оказался нехороший: поле в исходной статье фиксированной ширины и вылезает за границы. Но наш IDE для p5py
адаптирован под мобильные. Давайте сразу поправим код. Просто добавим авторасчёт ширины и высоты под текущий экран. Заменим это:
cell_width = 15
column_count = 80
row_count = 45
size(column_count * cell_width, row_count * cell_width)
на это:
cell_width = 15
# Проверяем условия для установки размеров окна
# Предположим, что 600 — это ширина типичного мобильного устройства
if display_width < 600:
w = display_width
h = display_height * 2 // 3
else:
w = display_width * 2 // 4
h = display_height * 2 // 4
# Рассчитываем количество строк и столбцов
column_count = w // cell_width
row_count = h // cell_width
w = column_count * cell_width
h = row_count * cell_width
size(w, h)
И вот, теперь можно кликать и с мобильных: сюда.
Что можно сделать еще?
Давайте улучшим usability.
Следующим шагом может быть простая доработка: «Сделать так, чтобы надпись исчезла после первого нажатия мышкой. Ведь пользователь уже прочитал инструкцию и выполнил необходимые действия».
Тому, кто только начал изучать Python, может быть интересно сделать такое задание самостоятельно. При желании делитесь своими решениями и улучшениями в комментариях.
Примечание
Модуль p5py
имеет множество ограничений. В отличие от Godot, он не предназначен для больших и средних проектов. Зато хорош для маленьких демонстраций и в учебных целях.
Сообщество. Там, где трудно одному...
Справлюсь вместе с вами. Мини-IDE и p5py
получились хорошими и добрыми. Решил потихоньку-понемногу собирать сообщество вокруг p5py
. Вот мой старенький и почти пустой Telegram-канал: t.me/p4kids. Попробую собрать заинтересованных учителей, преподавателей и родителей вокруг этой технологии.