Царство грибов. Симуляция мицелия на p5py. Битвы гифов. Часть первая

Одни из самых долгоживущих, самых скрытных и самых древних организмов на Земле. Грибы. Существа в скрытом царстве под горой. Они меня всегда увлекали.

В 1998 году внимание биологов привлекла гибель деревьев, чьи корни были опутаны грибницей. Тогда-то они и определили, что скопления опёнка темного в Орегоне не отдельные грибницы, а единый организм. Крупнейшее живое существо на Земле: размером с 880 гектаров и старше 2,4 тысячи лет.

Хочется написать симуляцию этого великолепного царства (прямо в браузере на Python и p5py). Посадить электронные споры, понаблюдать за ростом мицелия и восшедшими плодовыми телами, и проследить за спорами-путешественниками, как они создают новые колонии.

Добро пожаловать в путешествие в Царство Грибов.

image

Основной план прост: мицелий как-то прорастает и развивается, добывая ресурсы из почвы.

  • Часть 1. Подготовка

  • Часть 2. Ускорение и рефакторинг

  • Часть 3. Окрестность Мура

  • Часть 4. Борьба кланов

Часть 1. Подготовка

Мицелий:

  • представлен сетью (графом) из узлов;

  • растёт, извлекая питательные вещества из окружающей среды;

  • распространяется по соседним клеткам, если хватает ресурсов;

  • носитель генома (в будущем, сейчас пока без него).

class Mycelium:
    def __init__(self, x, y):
        self.nodes = [(x, y)]  # Координаты узлов
        self.energy = 100      # Энергия для роста
        self.age = 0

    def grow(self, environment):
        # Логика роста мицелия в зависимости от ресурсов и генома
        new_nodes = []
        for x, y in self.nodes:
            if self.energy > 0:
                neighbors = get_neighbors(x, y)  # Соседние клетки
                for nx, ny in neighbors:
                    if environment[nx][ny].resources > 0:
                        self.nodes.append((nx, ny))
                        environment[nx][ny].resources -= 1
                        self.energy -= 1
        self.age += 1

Окружающая среда:

  • сетка клеток, каждая из которых имеет параметры: ресурсы, свет, влажность и т. д. (для начала просто абстрактные ресурсы);

  • каждый такой участок почвы содержит ресурсы, которые будут уменьшаться по мере потребления мицелием.

class Environment:
    def __init__(self, width, height):
        self.grid = [[Cell(random.randint(50, 150)) for _ in range(width)] for _ in range(height)]
        self.width = width
        self.height = height

class Cell:
    def __init__(self, resources):
        self.resources = resources

Визуализация в p5py:

def setup():
    global environment, mycelia
    size(800, 800)
    environment = Environment(100, 100)
    mycelium = Mycelium(5, 5)  # Создаем мицелий в центре

def draw():
    global environment, mycelium
    background(0)  # Черный фон
    environment.display()  # Отображаем среду
    mycelium.grow(environment)  # Рост мицелия
    mycelium.display()  # Отображение мицелия

Соединяем всё в рабочий код

С основными сущностями всё понятно, но как бы это всё наглядно увидеть? С чего бы начать? Было бы здорово увидеть мицелий в виде тонких нитей, прорастающих в среде.

Мицелий. Будем использовать прямые, соединяющие узлы мицелия. Каждый узел связан с соседними, создавая сеть.

self.links = []  # Связи между узлами

И отображаем:

def display(self):
    stroke(200, 150)  # Белые линии с прозрачностью
    stroke_weight(1)
    for (x1, y1), (x2, y2) in self.links:
        line(x1 * 80 + 40, y1 * 80 + 40, x2 * 80 + 40, y2 * 80 + 40)

Да, у нас будет примитивная реализация хранения линий, просто, чтобы посмотреть. Если нам всё понравится, можно будет когда-нибудь пересмотреть структуру хранения гифов (нитей мицелия), хранить их в виде графа — это может быть полезно, если моделировать потом передачу полезных веществ с разных концов мицелия.

Среда. Отрисуем её в виде тепловой карты, где насыщенность цвета указывает на количество ресурсов.

def display(self):
    for x in range(self.width):
        for y in range(self.height):
            cell = self.grid[x][y]
            fill(0, cell.resources, 0, 100)  # Зеленый цвет в зависимости от ресурсов
            no_stroke()
            rect(x * 80, y * 80, 80, 80)

Объединяем всё вместе и получаем:

image

Код

Вроде первый шаг удался (хотя есть, что порефакторить). Есть земля и ресурсы, мицелий растёт. Запустите код по ссылке выше прямо в браузере. А для телефона потом сделаем адаптацию: размер экрана будет определяться автоматически, а ячейки будут поменьше.

Аниматоры, ваш выход!

Перезапускать код весело, но очень хочется, чтобы мицелий рос не скачком, а плавно. Анимируем его одной командой: добавим return при обработке роста, чтобы за один ход обрабатывался только один узел. Неэффективно, но визуально всё быстро проверим.

def grow(self, environment):
    if self.energy <= 0:
        return  # Если энергия закончилась, выходим
    
    # Обрабатываем только один узел за вызов
    for (x, y) in self.nodes:
        neighbors = get_neighbors(x, y, environment.width, environment.height)
        for nx, ny in neighbors:
            if environment.grid[nx][ny].resources > 40 and (nx, ny) not in self.nodes:
                self.nodes.append((nx, ny))
                self.links.append(((x, y), (nx, ny)))
                environment.grid[nx][ny].resources -= 1
                self.energy -= 1
                return  # Выходим после обработки одного узла
    self.age += 1

Да, тут не оптимально пока что, каждый раз заново пробегаем по всем узлам. На следующем шаге сделаем рефакторинг, а пока просто проверим анимацию — понравится она нам или нет. Хотя эта неоптимальность может быть и фичей, если окружающая среда меняется и нам нужно каждый раз заново пересматривать все узлы.

Добавим ещё точки на концах связей, временно, для наглядности.

image

Мне нравится, как мицелий огибает слева бедную на удобрения почву.

Позапускайте, как у вас будет расти мицелий?

Код

А ещё можно отнимать у земли больше ресурсов при росте:

environment.grid[nx][ny].resources -= 80

Получается более наглядно — ресурсы в почве уменьшаются, земля темнеет, мицелий захватывает мир:

image

Часть 2. Ускорение и рефакторинг

Слишком много магических чисел. Давайте вынесем в константы хотя бы размер ячейки:

# Константы
CELL_SIZE = 20  # Размер ячейки

И сделаем так, чтобы количество ячеек по горизонтали и вертикали рассчитывалось автоматически, исходя из ширины экрана и размера ячейки:

class Environment:
    def __init__(self):
        self.width = width // CELL_SIZE  # Количество ячеек по ширине
        self.height = height // CELL_SIZE  # Количество ячеек по высоте

Теперь удобнее менять размеры экрана и ячейки. Можно сделать мир побольше:

image

Код

Даже так интересно поэкспериментировать с количеством ресурсов в клетке, чтобы мицелий рос. Потребуем более богатую почву (было 40, стало 100):

if environment.grid[nx][ny].resources > 100

Мицелий растёт уже интереснее, более причудливо огибает бедную почву:

image

Ускоряемся

Но работает медленно. Наивная реализация роста нам не подходит. Нужно ускорить и оптимизировать поиск и расчёт новых узлов мицелия. 20 FPS в начале, плавно падает до 5 FPS, и в конце 15 FPS. Медленно.

Первый шаг

Самый простой способ: будем рисовать поле каждый десятый кадр. Визуально редкое обновление не особо заметно, но это сильно ускорит отображение:

if frame_count % 10 == 0:
    background(0)
    environment.display()

Код

Начинается теперь с 60 FPS, затем снова падает до 5 FPS и заканчивает на 30 FPS. Это очевидное улучшение.

Второй шаг. Поиск в ширину

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

self.current_index = 0  # Текущий индекс в массиве nodes

Заменим наш прошлый аляповатый цикл for вместе с brake:

for (x, y) in self.nodes:

на простое получение текущего узла:

x, y = self.nodes[self.current_index]  # Получаем узел по индексу
self.current_index += 1  # Увеличиваем индекс для обработки следующего узла

Код

Начинает теперь с 60 FPS и далее плавно падает до 30 FPS. Вот, уже неплохо.

— Немного алгоритмики этому господину.
Наше решение напоминает поиск в ширину (BFS), в котором дополнительная память, обычно очередь, необходима для отслеживания дочерних узлов, которые были обнаружены, но ещё не исследованы. Но нам пока достаточно просто указателя. Мы ведь и так послойно сохраняем найденные, но не исследованные узлы.

09ddb8b9bd9c04d505435ca33fb446e6.gif

Чёрный — исследовано, серый: поставлено в очередь на исследование.
Почему мы используем поиск в ширину, а не в глубину (DFS)? Очевидно, что грибнице не выгодно бесконечно исследовать мир одним отростком, если рядом есть много подходящих ресурсов. Транспортировка питательных веществ от слишком длинного отростка явно проигрывает коротким гифам.

Третий шаг

Заменим вложенные кортежи:

self.links.append(((x, y), (nx, ny)))

на более длинную строку:

self.links.append((x * self.size + self.size / 2, y * self.size + self.size / 2, nx * self.size + self.size / 2, ny * self.size + self.size / 2))

Зато отображение у нас станет совсем кратким:

for l in self.links:
    x1, y1, x2, y2 = l
    line(x1, y1, x2, y2)

Почему сделали такую замену? Дело в том, что Brython (онлайн-транслятор Python в JS) медленно работает с кортежами. И, кстати, от последнего кортежа тоже можно избавиться, это ускорит код ещё в полтора раза.

Стало стабильно 60–70 FPS.

Скрытый баг

Сейчас энергии у мицелия мало, и она заканчивается раньше, хотя ещё есть узлы, которые можно обойти. Но если энергии добавить до тысячи self.energy = 1220, то получим ошибку IndexError: 'list index out of range' в этой строке:

x, y = self.nodes[self.current_index]

Поэтому добавим проверочку:

if self.current_index >= len(self.nodes):
    return

Исправили:
Код

image

Завораживающе растёт

image

Четвёртый шаг. Несколько тактов за раз

Можно визуализацию ещё ускорить, если обрабатывать несколько узлов за раз. Просто введём счетчик узлов:

step = 0
while self.current_index < len(self.nodes) and self.energy > 0 and step < 4:
    step += 1
    # ...

Код

image

Гляди в корень

Добавим отображение растущих окончаний мицелия:

def display(self):
    for x, y in self.nodes[self.current_index:]:
        fill("red")
        rect(x*self.size, y*self.size, self.size, self.size)

imageimage

Код

Пятый шаг. Ещё рефакторинг

Функция get_neighbors() должна явно принадлежать классу Environment. Она была такой:

def get_neighbors(x, y, width, height):
    neighbors = []
    for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
        nx, ny = x + dx, y + dy
        if 0 <= nx < width and 0 <= ny < height:
            neighbors.append((nx, ny))
    return neighbors

Станет такой:

def get_neighbors(self, x, y):
    neighbors = []
    for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
        nx, ny = x + dx, y + dy
        if 0 <= nx < self.width and 0 <= ny < self.height:
            neighbors.append((nx, ny))
    return neighbors

И строка neighbors = get_neighbors(x, y, environment.width, environment.height) станет лаконичнее:
neighbors = environment.get_neighbors(x, y)

А ещё добавим мелочь в виде адаптации к размеру экрана:

if window_width > 600:
    size(800, 800)
else:
    size(window_width, window_height)

Код

Шестой шаг. Насыпем немного Dependency Injection

Мицелий у нас всегда растёт в определённой среде, поэтому сделаем внедрение зависимости сразу при создании его экземпляра. Для этого нам нужно модифицировать конструктор класса Mycelium. Передадим объект среды в качестве аргумента в конструктор:

environment = Environment()
mycelium = Mycelium(environment.width // 2, environment.height // 2, environment)  # Передаем среду
class Mycelium:
    def __init__(self, x, y, environment):
        self.environment = environment  # Сохраняем ссылку на среду
И упростится вызов `grow()`. Вместо mycelium.grow(environment) станет просто mycelium.grow().

Код

Часть 3. Окрестность Мура

А почему у нас всё такое квадратное? Больше напоминает схему. Можно же вместо окрестности Фон Неймана использовать окрестность Мура:

image

Окрестность Мура

Вот так мы расширим перебор:

# for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:

И тогда наш мицелий выглядит более натурально:

imageimage

Вид с высоты птичьего полёта

image

Если приблизиться

Код

Время кушать

Мицелий каждый ход будет высасывать ресурсы из завоёванных клеточек и пополнять свою энергию: self.energy += 0.034. Ну и тратить энергию на каждый свой узел: self.energy -= 0.01.

for nx, ny in self.nodes:
    self.energy -= 0.01
    if environment.grid[nx][ny].resources > 0:
        environment.grid[nx][ny].resources -= 0.5
        self.energy += 0.034

Этот баланс довольно требовательный. Попробуйте поэкспериментировать. Если получать хотя бы на 0,001 меньше энергии с ячейки, то мицелию уже не хватает энергии для роста. А если на 0,001 больше, то он уже неконтролируемо набирает десятки тысяч единиц энергии.

Можем даже начать отслеживать энергию мицелия в левом верхнем углу:

def show_energy(self):
    fill(255)
    rect(0, 0, 50, 10)
    fill(0)        
    text(int(self.energy), 10, 10)

Код

imageimage

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

environment.grid[nx][ny].resources -= 1.5

И сначала мицелий растёт интенсивно, а потом еды начинает не хватать и грибница быстро загибается.

Код

Часть 4. Борьба

Давайте запустим на площадку два мицелия. Просто напишем так:

from p5py import *
import random

run()

CELL_SIZE = 20

class Mycelium:
    ...

class Environment:
    ...

class Cell:
    ...

# Глобальные переменные
environment = None
mycelium1 = None
mycelium2 = None

def setup():
    global environment, mycelium1, mycelium2
    environment = Environment()
    mycelium1 = Mycelium(environment.width // 3, environment.height // 3, environment)
    mycelium2 = Mycelium(environment.width * 2 // 3, environment.height * 2 // 3, environment)

def draw():
    if frame_count % 10 == 0:
        background(0)
        environment.display()
    mycelium1.grow() 
    mycelium1.display()
    mycelium2.grow()
    mycelium2.display()

image

Код

Лучше, конечно же, универсально использовать массивы. Если будет желание, вы сможете поэкспериментировать, благо в online-IDE не требуется установка и настройка.

Пусть у мицелия при инициализации будет собственный цвет:

def __init__(self, x, y, environment, color):
    ...
    self.color = color  # Уникальный цвет для мицелия

def setup():
    ...
    mycelium1 = Mycelium(environment.width // 3, environment.height // 3, environment, color(255, 100, 100))
    mycelium2 = Mycelium(environment.width * 2 // 3, environment.height * 2 // 3, environment, color(100, 100, 255))

image

Код

Можно повысить требования к ресурсам:

if self.environment.grid[nx][ny].resources > 110

Тогда и гифы получаются более разреженные:

image

Код

А что если сделать так, чтобы мицелии появлялись в случайном месте карты?

def setup():
    global environment, mycelium1, mycelium2
    environment = Environment()
    
    # Случайные координаты для mycelium1 и mycelium2
    mycelium1_x = rand(0, environment.width)
    mycelium1_y = rand(0, environment.height)
    mycelium2_x = rand(0, environment.width)
    mycelium2_y = rand(0, environment.height)
    
    mycelium1 = Mycelium(mycelium1_x, mycelium1_y, environment, color(255, 100, 100))
    mycelium2 = Mycelium(mycelium2_x, mycelium2_y, environment, color(100, 100, 255))

Не совсем ещё битва, но синий явно успел окружить красного:

image

Код

А здесь они появились рядом и сплелись:

image

Красный загнулся раньше

Мы не запрещаем расти двум мицелиям на одном участке. Пока что.

Код

У нас сейчас отображение энергии разных мицелиев наслаивается друг на друга. Надо бы исправить. Поменяем это фиксированное положение:

def show_energy(self):
    fill(255)
    rect(0, 0, 50, 10)
    fill(0)        
    text(int(self.energy), 10, 10)

скажем, на такое:

def show_energy(self):
    # Позиционирование энергии относительно мицелия
    energy_x = self.nodes[0][0] * self.size + 10  # Позиция x для текста энергии
    energy_y = self.nodes[0][1] * self.size + 10  # Позиция y для текста энергии
    fill(255)
    rect(energy_x, energy_y, 50, 10)
    fill(0)        
    text(int(self.energy), energy_x + 5, energy_y + 8)

И вместо того чтобы всегда отображать энергию в одном и том же месте (0, 0), мы можем позиционировать текст в зависимости от положения мицелия на экране.

imageimage

Количество энергии сейчас отображается некрасиво: белый фон отвлекает, давайте исправим: сделаем фон чёрным, а число — белым:

def show_energy(self):
    energy_x = self.nodes[0][0] * self.size + 10
    energy_y = self.nodes[0][1] * self.size + 10
    
    fill(0)
    no_stroke()
    rect(energy_x, energy_y, 60, 20)
    
    fill(255)
    text_size(12)
    text_align(CENTER)
    
    text(int(self.energy), energy_x + 30, energy_y + 15)  # Центрируем текст

image

Код

В общем я за вас немного позапускал код :) Здесь разные мицелии стали расти из одного угла. Интересно, кто победит:

image

Похоже, синий:

image

А здесь обратная ситуация, красный опережает:

imageimage

Можно уменьшить размер ячеек, но если мицелии друг от друга далеко, то успеют загнуться самостоятельно:

image

А если вблизи, то начинается конкуренция:

imageimage

Хорошо бы внести какие-то изменения в среду или сам мицелий.

Улучшения

Самое простое: сделаем цвета случайными:

mycelium1 = Mycelium(mycelium1_x, mycelium1_y, environment, color(rand(0, 255), rand(0, 255), rand(0, 255)))
mycelium2 = Mycelium(mycelium2_x, mycelium2_y, environment, color(rand(0, 255), rand(0, 255), rand(0, 255)))

Лучше бы перенести инициализацию в конструктор.

imageimageimage

Код

Исправляем ошибку

Кстати, с момента случайного расположения мицелия на карте он перестал находиться ровно в центре клетки. Может, вы это заметили раньше. Дело в том, что rand() возвращает не целое число:

mycelium1_x = rand(0, environment.width)
print(mycelium1_x)

Поправим:

mycelium1_x = int(rand(0, environment.width))
mycelium1_y = int(rand(0, environment.height))
mycelium2_x = int(rand(0, environment.width))
mycelium2_y = int(rand(0, environment.height))

Код

Хотя можно было бы считать это фичей, а не багом, так как теперь мицелии не так красиво сплетаются друг с другом, а просто упираются рогами (гифами):

imageimage

Сейчас цвета двух мицелиев легко сливаются. Сделаем их более контрастными?

# Генерация более контрастных цветов
mycelium1_color = color(rand(200, 255), rand(0, 100), rand(0, 100))  # Теплые оттенки
mycelium2_color = color(rand(0, 100), rand(100, 255), rand(200, 255))  # Холодные оттенки

mycelium1 = Mycelium(mycelium1_x, mycelium1_y, environment, mycelium1_color)
mycelium2 = Mycelium(mycelium2_x, mycelium2_y, environment, mycelium2_color)

Выживание. Пожиратели

Давайте придумаем, как сделать так, чтобы разные мицелии сражались друг с другом за выживание. Пусть один мицелий может прорастать в другой. Например, при поиске следующего узла воспринимает ресурсом не только содержимое земли, но и местоположение узла «вражеского» мицелия. Если он найден, то нужно удалить вражеский узел и его связи, а самому там вырасти.

Перебирая узлы, добавим проверку:

if (self.environment.grid[nx][ny].resources > 110 or (nx, ny) in other_mycelium.nodes) and (nx, ny) not in self.nodes:

И сам акт поедания:

if (nx, ny) in other_mycelium.nodes:  # Если находим узел другого мицелия
    other_mycelium.nodes.remove((nx, ny))  # Удаляем узел
    other_mycelium.energy -= 1  # Урезаем энергию
    # Также удаляем все связи, которые имел этот узел
    other_mycelium.links = [link for link in other_mycelium.links if link[0] != nx * self.size + self.size / 2 or link[1] != ny * self.size + self.size / 2]

Всё вместе:

for nx, ny in neighbors:
    if (self.environment.grid[nx][ny].resources > 110 or (nx, ny) in other_mycelium.nodes) and (nx, ny) not in self.nodes:
        self.nodes.append((nx, ny))
        self.links.append((x * self.size + self.size / 2, y * self.size + self.size / 2, nx * self.size + self.size / 2, ny * self.size + self.size / 2))
        self.environment.grid[nx][ny].resources -= 10
        self.energy -= 1
        
    if (nx, ny) in other_mycelium.nodes:  # Если находим узел другого мицелия
        other_mycelium.nodes.remove((nx, ny))  # Удаляем узел
        other_mycelium.energy -= 1  # Урезаем энергию
        # Также удаляем все связи, которые имел этот узел
        other_mycelium.links = [link for link in other_mycelium.links if link[0] != nx * self.size + self.size / 2 or link[1] != ny * self.size + self.size / 2]

То есть мы проверяем, является ли соседний узел узлом другого мицелия. Если да, то удаляем этот узел и все его связи с чужим мицелием.

imageimage

Но когда один полностью съедает другого, у нас возникает ошибка IndexError: 'list index out of range'.

Она возникает в методе display() класса Mycelium из-за того, что когда один мицелий полностью «съедает» другой, то его узлы истощаются, и при попытке доступа к элементам списка узлов через self.nodes[0] может возникнуть ситуация, когда этот самый self.nodes оказывается пустым.

Чтобы избежать этой ошибки, добавим проверку на наличие узлов перед тем, как обращаться к self.nodes[0]. Например, так:

def show_energy(self):
    if not self.nodes:  # Проверка на наличие узлов
        return  # Если нет узлов, ничего не делаем
    energy_x = self.nodes[0][0] * self.size + 10
    energy_y = self.nodes[0][1] * self.size + 10
    fill(0)
    no_stroke()
    rect(energy_x, energy_y, 60, 20)
    fill(255)
    text_size(12)
    text_align(CENTER)
    text(int(self.energy), energy_x + 30, energy_y + 15)

imageimageimageimage

Код

Теперь бы отмершему мицелию почву удобрять. Да новым мицелиям по клику появляться. Попробуете это сделать?

Ну как же без редактора

А давайте напоследок сделаем так, чтобы мицелии хранились в массиве. И при клике мышки начинал развиваться новый мицелий.

Создадим массив для хранения мицелиев. Заменим отдельные переменные mycelium1 и mycelium2 на массив myceliums:

myceliums = []  # Список для хранения мицелиев
# Создание и добавление двух начальных мицелиев
for _ in range(2):
    mycelium_x = int(rand(0, environment.width))
    mycelium_y = int(rand(0, environment.height))
    mycelium_color = color(rand(200, 255), rand(0, 100), rand(0, 100))
    myceliums.append(Mycelium(mycelium_x, mycelium_y, environment, mycelium_color))

Добавим обработчик события мыши: используем метод mouse_clicked(), чтобы добавлять новый мицелий по координатам клика:

def mouse_clicked():
    # Добавление нового мицелия в место клика
    mycelium_x = mouse_x // CELL_SIZE
    mycelium_y = mouse_y // CELL_SIZE
    mycelium_color = color(rand(100, 200), rand(150, 255), rand(50, 150))
    myceliums.append(Mycelium(mycelium_x, mycelium_y, environment, mycelium_color))

Обновим методы роста и отображения мицелиев: пройдём по массиву в методе draw() и вызовем соответствующие методы роста и отображения для каждого мицелия:

def draw():
    if frame_count % 10 == 0:
        background(0)
        environment.display()
    # Рост и отображение всех мицелиев
    for i in range(len(myceliums)):
        mycelium = myceliums[i]
        for other_mycelium in myceliums:
            if other_mycelium != mycelium:
                mycelium.grow(other_mycelium)
        mycelium.display()

Код

И получаем такие сражения:

imageimageimageimageimageimage

Запускайте код по ссылке выше, чтобы поэкспериментировать.

Итого

  • Мы написали простую симуляцию мицелия, представленного сетью узлов, который живёт в окружающей среде из сетки клеток с ресурсами.

  • Отрефакторили код для улучшения производительности и ускорения визуализации.

  • Разнообразили визуализацию и адаптировали её к разным размерам экрана.

  • Реализовали конкуренцию между мицелиями, добавив возможность «поедания» друг друга.

  • Добавили разнообразия: случайные стартовые положения и цвета.

Получилось много гифок и кода, который вы можете запустить в один клик в браузере. Если вам было интересно, то это может быть хорошим фундаментом в дальнейших экспериментах с симуляцией Царства Грибов.

Если вам понравилось быстро тестировать гипотезы в браузере, то вот ещё статьи про p5py:

Про темы, связанные с p5py иногда (редко) пишу в канале @p4kids

© Habrahabr.ru