Игра «Жизнь» Конвея
Четыре строчки правил, поле из живых и мёртвых клеток — и вот по экрану ползёт маленькая фигура, не теряя формы. Игра «Жизнь» Конвея показывает, как из примитивных законов рождается почти живое поведение.
Игра «Жизнь» — это двумерный клеточный автомат с двумя состояниями (клетка жива или мертва) и окрестностью Мура (8 соседей), где судьба клетки на следующем шаге определяется только числом её живых соседей по трём простым правилам.
Придумал её в 1970 году математик Джон Конвей. «Игрой» её называют по традиции: игрок не делает ходов, он лишь задаёт начальную расстановку и смотрит, как поле эволюционирует само. Это не развлечение ради развлечения — «Жизнь» оказалась настолько богатой, что в ней удаётся построить логические схемы и даже модель универсального компьютера. Нам она интересна как чистейший пример эмерджентности: правила тривиальны, поведение — нет.
Правила Игры «Жизнь»
Для каждой клетки считаем число живых соседей среди восьми (окрестность Мура). Затем применяем правила:
- Выживание. Живая клетка с 2 или 3 живыми соседями остаётся живой.
- Смерть. Живая клетка с менее чем 2 соседями умирает «от одиночества», с более чем 3 — «от перенаселения».
- Рождение. Мёртвая клетка ровно с 3 живыми соседями оживает.
Всё. Эти три правила обычно записывают коротко как «B3/S23»: рождение (Birth) при 3 соседях, выживание (Survival) при 2 или 3. Заметьте: правило локальное и одинаковое для всех клеток, никто не управляет полем сверху.
Планер — фигура, которая движется
Самое знаменитое творение «Жизни» — планер (glider). Это группа из пяти живых клеток, которая каждые 4 поколения возвращается к своей форме, но смещённая на одну клетку по диагонали. То есть фигура «летит». Соберём поле 8x8, поставим планер в угол и посмотрим 4 поколения.
def life_step(grid):
rows = len(grid)
cols = len(grid[0])
new = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
n = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
rr, cc = r + dr, c + dc
if 0 <= rr < rows and 0 <= cc < cols:
n += grid[rr][cc]
if grid[r][c] == 1:
new[r][c] = 1 if n in (2, 3) else 0
else:
new[r][c] = 1 if n == 3 else 0
return new
def show(grid):
for row in grid:
print(''.join('#' if c else '.' for c in row))
print()
grid = [[0] * 8 for _ in range(8)]
for (r, c) in [(0, 1), (1, 2), (2, 0), (2, 1), (2, 2)]:
grid[r][c] = 1
print("Поколение 0:")
show(grid)
for gen in range(1, 4):
grid = life_step(grid)
print(f"Поколение {gen}:")
show(grid)
Вывод:
Поколение 0: .#...... ..#..... ###..... ........ ........ ........ ........ ........ Поколение 1: ........ #.#..... .##..... .#...... ........ ........ ........ ........ Поколение 2: ........ ..#..... #.#..... .##..... ........ ........ ........ ........ Поколение 3: ........ .#...... ..##.... .##..... ........ ........ ........ ........
Присмотритесь к фигуре: на каждом шаге она меняет форму, проходит через промежуточные состояния, но в целом «сползает» вправо и вниз. Если бы поле было больше и мы дали бы ещё шаги, планер продолжил бы лететь по диагонали бесконечно, пока не упрётся в край или другую фигуру. Никто не говорил клеткам «двигайтесь по диагонали» — это движение само сложилось из правил рождения и смерти.
Как работает под капотом
Сердце кода — двойной цикл по dr и dc со значениями (-1, 0, 1). Он перебирает все 9 клеток квадрата 3x3 вокруг текущей. Строчка if dr == 0 and dc == 0: continue пропускает саму клетку, оставляя ровно 8 соседей Мура. Так мы получаем n — число живых соседей.
Дальше работает синхронность: мы пишем в new, а читаем из старого grid. Если бы мы обновляли grid на месте, то уже изменённые клетки искажали бы подсчёт соседей для следующих, и планер бы «развалился». Проверка границ if 0 <= rr < rows and 0 <= cc < cols отбрасывает соседей за пределами поля — у нас «мёртвые» края, за полем жизни нет.
Логика перехода — это прямой перевод правил B3/S23 в код. Для живой клетки: 1 if n in (2, 3) else 0. Для мёртвой: 1 if n == 3 else 0. Две строки — и весь Конвей у вас в руках.
Частые ошибки
- Обновление на месте. Самая частая ошибка: пересчитывать соседей по той же сетке, которую уже меняете. Нужен отдельный
new— иначе фигуры искажаются. - Клетка считает саму себя. Если забыть
continueприdr == 0 and dc == 0, клетка попадёт в число своих соседей, и все правила «съедут». - Неверная окрестность. «Жизнь» использует 8 соседей (Мур). Если посчитать только 4, получится совсем другой автомат без планеров.
- Выход за границы. Без проверки
0 <= rr < rowsиндекс-1в Python тихо завернётся на противоположный край, и поле «склеится» в тор там, где вы этого не хотели.
Итоги
- Игра «Жизнь» — двумерный автомат с двумя состояниями и окрестностью Мура.
- Правила B3/S23: живая клетка выживает при 2–3 соседях, мёртвая оживает ровно при 3.
- Планер (glider) — фигура из 5 клеток, которая за 4 поколения сдвигается на одну клетку по диагонали.
- Локальные правила порождают глобальное движение — клеткам никто не приказывал «лететь».
- Главные технические детали: синхронное обновление через буфер, пропуск самой клетки и явная проверка границ.