Would you like me to show you the magic of live code on p5py?
Inspired articlededicated to writing a cellular automaton in Godot and exporting the project to HTML, I want to show you how to use a modern online engine for these purposes p5py
. The code is alive not only because we are talking about the game of Life, but also because of the way it was developed and launched. Everything is very alive!
Black cloak
TL;DR: the final project is here Here. Just click and it will appear.
What's the magic?
We will get a similar result, but much faster and more fun. We won't have to set up or download anything, which is great for beginners. Just follow the links in the article and run working examples.
We won't need export to HTML either, since the code is in Python, thanks to
p5py
and online IDE, runs directly in the browser.Moreover, if you have a creative idea on how to improve the code and you implement it, then: a) you will immediately see the result, b) by clicking “Save”, you will receive a ready-made link that you can share with friends or in the comments.
p5py
This is an adaptation of the popular Processing (p5.js) for Python. I wrote it for conducting classes in children’s programming groups and for the book, which I already talked about in the article “As I wrote a book for children: “Mom, don’t distract. I'm learning Python!“
An online IDE has also been developed for it, which can be launched from all the links in this article. There are only two buttons: “Run” and “Save”. Don't get confused.
We get
Instead of GDScript – the most standard Python (+ module
p5py
).No separate export is needed – the result is immediately available via an online link.
Live sandbox – code automatically reruns in live mode as it is written (can be disabled if desired).
The size is not a gigabyte, but 4.5 megabytes.
And he was in love with Life…
Let's recreate Conway's “Game of Life” – a cellular automaton capable of generating complex patterns that imitate life.
Source code on 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
Our code for p5py
:
row_count = 45
column_count = 80
cell_width = 15
Why so short? And because, because, because…
…that we take a project-oriented approach and don't write lines of code that won't be useful in the next step. Why keep them in your head? They only create noise. For now, all we need to know is the field size 45х80
and cell size 15х15
.
Field, field, field…
White, white let's make an empty field of cells.
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)
# Здесь мы просто отображаем пустое поле
Click Hereto run. If you suddenly get an error, please write in a personal message. The version is still 0.XXX, there may be bugs.
Experiment! Change the background and line colors. For example,
background(200, 100, 0)
is a shade of orange, andstroke(255, 120, 0)
– shade of red.
Chance will decide how to fill the cells
Let's improve the original code:
We immediately fill the field with random values - this makes it more interesting for the player to watch the process. When you start it, something happens immediately.
And since in the online-IDE
p5py
easy to press the cross, closing the program, and immediately pressRUN
having launched it again, you can play with different starting conditions.
# Инициализация состояния клеток случайными значениями
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)
Like this it worked. Here I made the cells yellow. You can experiment with the code live, for example by replacing the number 15 with another one to change the density of the field.
The preparation is complete, now we have both a grid and randomly colored cells.
It's time to move on to…
Field update. Game not by the rules
Let's add a function update_game_state()
which will simply go through each cell of the field and write a new status into it. Which, in turn, learns from the function 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
And here it is link to play around.
Yes, yes, the game is not by the rules yet. We use a mock (stub) to quickly see the result and it would be interesting to improve the program step by step.
Now let's move on to…
Game rules
And here comes the most important thing – creating the rules for the game of Life. We write functions to count living neighbors and determine the future state of each cell. These are the classic Conway rules: a cell becomes alive if it has exactly three living neighboring cells, and remains alive if it has two or three living neighboring cells.
Source code on 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
Almost the same as 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
But also a function update_game_state()
we will also have to change a little to save the previous value of the cells into a temporary array.
What do you think this step is for?
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)
And if you want more Python style, then replace it with previous_cell_states = [row.copy() for row in cell_matrix]
.
Param-pam-pam
Well that's it! Here it is, ours ready code.
NoDB: bug or feature
You have already noticed that the link to our code is long, like sentences in Julio Cortazar's books. But that's how it's meant to be. This is a modern NoDB approach (more precisely, URL-based storage), when small programs are slightly compressed and stored directly in the URL. Small, lieutenant, I said small!
In general, a few hours before publication previous article The main database broke down, and I temporarily put the project on the alternate NoDB airfield, which was ready earlier. As soon as time appears, I will return saving to a permanent database and short links.
Let's add interactivity
In the original article, the user could draw creatures on the field himself. Let's do the same on p5py
but only slightly improve the use of the source code:
Let’s immediately turn on the “intervention” mode. Let the player be able to immediately add new pieces:
is_game_running = True
.Let's remove the on/off button for this drawing, since it is unclear why it is needed. Only the interface overloads.
Let's replace the game stop button with… an automatic action. By clicking the mouse, the player can draw a new figure, and the game pauses for this time. As soon as the player releases the mouse, the game resumes. Yes, here I consciously sacrifice the opportunity to draw many figures at once during a pause, but we have a demo, and for a demo it will be more intuitive.
Add to def draw():
…hint:
fill(255, 140)
text_size(20)
text("Вы можете нарисовать фигуру мышкой", 30, 30)
…and start/stop the game:
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. Draw…
When mobile friends are with me
If you suddenly read this article on a mobile phone and clicked on one of the links above, the result was not good: the field in the original article is of a fixed width and extends beyond the boundaries. But our IDE is for p5py
adapted for mobile. Let's fix the code right away. Let's just add automatic calculation of width and height for the current screen. Let's replace this:
cell_width = 15
column_count = 80
row_count = 45
size(column_count * cell_width, row_count * cell_width)
to this:
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)
And now you can click from mobile phones: here.
What else can be done?
Let's improve usability.
The next step could be a simple modification: “Make the inscription disappear after the first mouse click. After all, the user has already read the instructions and completed the necessary actions.”
For someone who has just started learning Python, it might be interesting to do this task yourself. If you wish, share your solutions and improvements in the comments.
Note
Module p5py
has many limitations. Unlike Godot, it is not designed for large and medium-sized projects. But good for small demonstrations and educational purposes.
Community. Where it is difficult for one…
I can handle it together with you. Mini IDE and p5py
turned out to be good and kind. I decided to slowly, little by little, gather the community around p5py
. Here is my old and almost empty Telegram channel: https://t.me/p4kids. I will try to gather interested teachers, lecturers and parents around this technology.