Идея вынашивалась довольно давно и сегодня я хочу рассказать о процессе создания симуляции экосистемы под рабочим названием "NewLife", которая моделирует взаимодействие между травой, мирными клетками и хищниками. Идея симуляции, как понятно из названия статьи, родилась по мотивам игры Life из доисторической компьютерной эпохи.

Идея и постановка задачи

Целью задачи является создать простую, но, прежде всего, наглядную модель экосистемы, способную заинтересовать ребенка, где три типа сущностей взаимодействуют друг с другом:

  1. Трава — растет случайным образом и служит пищей для мирных клеток.

  2. Мирные клетки — питаются травой, размножаются и служат пищей для хищников.

  3. Хищники — охотятся на мирные клетки и размножаются.

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

Визуальная часть основана на возможностях pygame

Итак, переходим к кодингу.

Начало разработки

Инициализация Pygame

import pygame 
import random 
from collections import deque

Инициализация Pygame

Первым шагом инициализируем Pygame и создание окна для отображения симуляции. Мы задали размеры окна, параметры клеток и цвета для каждого типа сущностей.

pygame.init()

width, height = 200, 200
cell_size = 4
simulation_height = height * cell_size
graph_height = 400
window_size = (width * cell_size, simulation_height + graph_height)
screen = pygame.display.set_mode(window_size)
pygame.display.set_caption("NewLife Simulation")

Создание классов сущностей

Для каждой сущности (трава, мирные клетки, хищники) создадим отдельный класс. Каждый класс содержит координаты и методы для движения, размножения и взаимодействия с другими сущностями. Такой подход позволит нам добавлять сущности в количествах, ограниченных только производительностью системы. Вопрос оптимизации оставим за скобками

class Grass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Peaceful:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.energy = 100

    def move_towards_grass(self, grass_list):
        if not grass_list:
            return
        closest_grass = min(grass_list, key=lambda g: (self.x - g.x) ** 2 + (self.y - g.y) ** 2)
        if self.x < closest_grass.x:
            self.x += 1
        elif self.x > closest_grass.x:
            self.x -= 1
        if self.y < closest_grass.y:
            self.y += 1
        elif self.y > closest_grass.y:
            self.y -= 1

    def reproduce(self, peaceful_list):
        if self.energy >= 30 and len(peaceful_list) < max_cells_per_cycle:
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                new_x, new_y = self.x + dx, self.y + dy
                if 0 <= new_x < width and 0 <= new_y < height:
                    if not any(p.x == new_x and p.y == new_y for p in peaceful_list):
                        peaceful_list.append(Peaceful(new_x, new_y))
                        self.energy /= 2
                        break

class Predator:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.energy = 100

    def move_towards_peaceful(self, peaceful_list):
        if not peaceful_list:
            return
        closest_peaceful = min(peaceful_list, key=lambda p: (self.x - p.x) ** 2 + (self.y - p.y) ** 2)
        if self.x < closest_peaceful.x:
            self.x += 1
        elif self.x > closest_peaceful.x:
            self.x -= 1
        if self.y < closest_peaceful.y:
            self.y += 1
        elif self.y > closest_peaceful.y:
            self.y -= 1

    def reproduce(self, predator_list):
        if self.energy >= 50 and len(predator_list) < max_cells_per_cycle:
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                new_x, new_y = self.x + dx, self.y + dy
                if 0 <= new_x < width and 0 <= new_y < height:
                    if not any(pd.x == new_x and pd.y == new_y for pd in predator_list):
                        predator_list.append(Predator(new_x, new_y))
                        self.energy /= 2
                        breakОсновной цикл симуляции

Основной цикл симуляции

Основной цикл игры будет у нас заниматься обновлением состояния симуляции и отрисовку всех элементов. В каждом цикле:

  1. Растет трава.

  2. Мирные клетки двигаются, едят траву и размножаются.

  3. Хищники охотятся на мирные клетки и размножаются.

  4. Состояние системы сохраняется для отображения на графике.

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                paused = not paused

    if not paused:
        # Логика игры
        # Трава растет
        for _ in range(grass_growth_per_cycle):
            if len(grass) < width * height:
                x, y = random.randint(0, width - 1), random.randint(0, height - 1)
                if not any(g.x == x and g.y == y for g in grass):
                    grass.append(Grass(x, y))

        # Мирные клетки двигаются и едят траву
        for p in peaceful[:]:  # Используем копию списка для безопасного удаления
            p.energy -= 1
            if p.energy <= 0:
                peaceful.remove(p)
                continue
            p.move_towards_grass(grass)
            for g in grass[:]:  # Используем копию списка для безопасного удаления
                if p.x == g.x and p.y == g.y:
                    grass.remove(g)
                    p.energy = 100
                    break
            p.reproduce(peaceful)

        # Хищные клетки двигаются и едят мирных
        for pd in predator[:]:  # Используем копию списка для безопасного удаления
            pd.energy -= 1
            if pd.energy <= 0:
                predator.remove(pd)
                continue
            pd.move_towards_peaceful(peaceful)
            for p in peaceful[:]:  # Используем копию списка для безопасного удаления
                if pd.x == p.x and pd.y == p.y:
                    peaceful.remove(p)
                    pd.energy = 100
                    break
            pd.reproduce(predator)

        # Ограничение размножения
        if len(peaceful) > max_cells_per_cycle:
            peaceful = peaceful[:max_cells_per_cycle]
        if len(predator) > max_cells_per_cycle:
            predator = predator[:max_cells_per_cycle]

        # Сохранение истории
        history.append((len(grass), len(peaceful), len(predator)))

        # Отрисовка
        draw_cells()
        graph_surface = draw_graph()
        screen.blit(graph_surface, (0, simulation_height))

        # Отображение информации
        font = pygame.font.SysFont("Arial", 18)
        text = font.render(f"Cycle: {cycle}, Grass: {len(grass)}, Peaceful: {len(peaceful)}, Predator: {len(predator)}", True, (255, 255, 255))
        screen.blit(text, (10, 10))

        pygame.display.flip()
        clock.tick(1)
        cycle += 1

Проблемы и их решение

Проблема 1: Утечка памяти из-за большого количества сущностей

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

max_cells_per_cycle = 500
grass_growth_per_cycle = 200

Проблема 2: Нереалистичное поведение хищников

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

pd.energy -= 1
if pd.energy <= 0:
    predator.remove(pd)
    continue

Проблема 3: Отрисовка графика

Я пробовал сначала отрисовывать график с использование mathplotlib, но оказалось, что это кушает ресурсы необъятных количествах, да и вообще не очень наглядно. В результате остановился на варианте с выводом графика снизу поля симуляции. НЕ очень пафосно, зато работает :-)

GitHub

Выводы и заключение

Создание симуляции «NewLife» оказалось весьма любопытным и показательным для юных обучающихся языку python. В процессе разработки удалось порешать различные проблемы, связанные с производительностью, реалистичностью поведения сущностей и визуализацией данных. В результате получилась простая, но наглядная модель экосистемы, которая может быть использована для изучения базовых принципов взаимодействия в природе. При этом, с указанными значениями стартовых параметров удалось получить «экосистему», дожившую до 2000+ цикла (дальше не хватило терпения ждать).

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

Комментарии (0)