Хотите, покажу вам магию живого кода на p5py?

от автора

Вдохновившись статьёй, посвящённой написанию клеточного автомата на Godot и экспорту проекта в HTML, хочу показать вам, как использовать для этих целей модерновый онлайн-движок p5py. Код живой не только потому, что мы про игру «Жизнь», но и благодаря способу его разработки и запуска. Всё очень живо!

godot

Чёрный плащ

TL;DR: финальный проект вот здесь. Только кликните, и он появится.

В чем магия?

  1. Мы получим похожий результат, но намного быстрее и увлекательнее. Нам не придётся ничего настраивать и скачивать, что отлично подходит для новичков. Просто переходите по ссылкам в статье и запускайте рабочие примеры.

  2. Экспорт в HTML нам тоже не понадобится, поскольку код на Python, благодаря p5py и онлайн-IDE, запускается прямо в браузере.

  3. Более того, если у вас возникнет творческая идея, как улучшить код, и вы её реализуете, то: а) сразу увидите результат, б) нажав «Сохранить», получите готовую ссылку, которой можно поделиться с друзьями или в комментариях.


p5py

Это адаптация популярного Processing (p5.js) для Python. Я написал его для проведения занятий в детских кружках по программированию и для книги, про которую уже рассказывал в статье «Как я написал книгу для детей: «Мама, не отвлекай. Я Python учу!».

godot

Для него же разработана и онлайн-IDE, которая запускается по всем ссылкам в этой статье. Там всего две кнопки: «Запустить» и «Сохранить». Не перепутаете.

Получаем

  1. Вместо GDScript — самый стандартный Python (+модуль p5py).

  2. Не нужен отдельный экспорт — результат сразу доступен по ссылке онлайн.

  3. Живая песочница — код автоматически перезапускается в живом режиме по мере его написания (по желанию можно отключить).

  4. Размер не гигабайт, а 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)   # Здесь мы просто отображаем пустое поле
empty

Нажмите здесь, чтобы запустить. Если вдруг выдало ошибку, напишите, пожалуйста, в личку. Версия всё ещё 0.XXX, могут быть баги.

Поэкспериментируйте! Поменяйте цвет фона и линий. Например, background(200, 100, 0) — это оттенок оранжевого, а stroke(255, 120, 0) — оттенок красного.

Случай решит, как заполнить ячейки

Улучшим оригинальный код:

  1. Сразу заполняем поле случайными значениями — так игроку интереснее наблюдать за процессом. При запуске сразу что-то происходит.

  1. А так как в 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)
random

Вот так получилось. Здесь я сделал ячейки жёлтого цвета. Можете поэкспериментировать с кодом вживую, например заменив число 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
change

А вот и ссылка на поиграться.

Да-да, пока игра не по правилам. Мы используем 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].

Парам-пам-пам

life4

Ну вот и всё! Вот он, наш готовый код.


NoDB: баг или фича

Вы уже заметили, что ссылка на наш код длинная, как предложения в книгах Хулио Кортасара. Но так задумано. Это современный подход NoDB (точнее, URL-based storage), когда небольшие программы немного сжимаются и сохраняются прямо в URL. Небольшие, поручик, я сказал, небольшие!

Вообще, за несколько часов перед публикацией предыдущей статьи сломалась основная база данных, и я временно посадил проект на запасной аэродром NoDB, который был готов раньше. Как появится время, верну сохранение в постоянной базе данных и короткие ссылки.


Добавим интерактивность

В оригинальной статье пользователь мог сам рисовать существ на поле. Давайте сделаем так же на p5py, но только немного улучшим использование исходного кода:

  1. Сразу включим режим «вмешательства». Пусть игрок сможет сразу добавлять новые фигуры: is_game_running = True.

  2. Уберём кнопку включения-выключения этого рисования, так как неясно, зачем она нужна. Только интерфейс перегружает.

  3. Заменим кнопку остановки игры на… автоматическое действие. При нажатии мышки игрок может нарисовать новую фигуру, а игра на это время приостанавливается. Как только игрок отпустит мышку — игра возобновляется. Да, здесь я осознанно жертвую возможностью в паузе нарисовать много фигур сразу, но у нас же демо, а для демо так будет интуитивно понятнее.

Добавим в 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. Рисуйте…

life7

Сылка на код


Когда мобайл-друзья со мной

Если вы вдруг читаете эту статью на мобильном и кликнули по одной из ссылок выше, то результат оказался нехороший: поле в исходной статье фиксированной ширины и вылезает за границы. Но наш 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)
life8_1

life8_1

И вот, теперь можно кликать и с мобильных: сюда.


Что можно сделать еще?

Давайте улучшим usability.

Следующим шагом может быть простая доработка: «Сделать так, чтобы надпись исчезла после первого нажатия мышкой. Ведь пользователь уже прочитал инструкцию и выполнил необходимые действия». 

Тому, кто только начал изучать Python, может быть интересно сделать такое задание самостоятельно. При желании делитесь своими решениями и улучшениями в комментариях.

Примечание

Модуль p5py имеет множество ограничений. В отличие от Godot, он не предназначен для больших и средних проектов. Зато хорош для маленьких демонстраций и в учебных целях.

Сообщество. Там, где трудно одному…

Справлюсь вместе с вами. Мини-IDE и p5py получились хорошими и добрыми. Решил потихоньку-понемногу собирать сообщество вокруг p5py. Вот мой старенький и почти пустой Telegram-канал: t.me/p4kids. Попробую собрать заинтересованных учителей, преподавателей и родителей вокруг этой технологии.


ссылка на оригинал статьи https://habr.com/ru/articles/854474/