Как стать автором
Обновить
578.21
Сбер
Технологии, меняющие мир

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

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров2.5K

Вдохновившись статьёй, посвящённой написанию клеточного автомата на 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. Попробую собрать заинтересованных учителей, преподавателей и родителей вокруг этой технологии.

Теги:
Хабы:
Всего голосов 11: ↑11 и ↓0+20
Комментарии7

Информация

Сайт
www.sber.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия