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