Skip to content
verbitsky-vladislavPublic

About

Evolution sandbox in the browser: creatures forage, grow, hunt each other and age — with no fitness function written anywhere

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Эволюция

Песочница для симуляций эволюции в браузере.

Приспособленность здесь нигде не записана формулой. Существа ищут еду, растут, охотятся друг на друга, стареют и оставляют потомство — а признаки популяции меняются сами, потому что больше потомков оставляет тот, кто съел больше. Скорость, размер, чутьё, сроки размножения и даже характер наследуются и подбираются отбором, и ни одно из равновесий не задано заранее.

▶ Открыть вживую

TypeScript + Vite + Canvas, без единой зависимости во время работы. Около тысячи строк, из них девятьсот — сам мир.

Быстрый старт

npm install
npm run dev

Откроется http://localhost:5173. Пробел — пуск/пауза, S — один шаг, R — сброс.

Что видно на экране

Элемент Что означает
Зелёные точки трава
Круги существа: радиус — нынешний размер тела
Цвет круга характер: зелёный тянется к траве, красный к добыче
Полупрозрачные дети, ещё не доросшие до взрослого размера
Бледные кольца радиус чутья
Графики справа летопись: население и средние значения признаков

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

Панель справа снизу — 30 ползунков, которые крутятся прямо во время прогона.

Что уже есть

Естественный отбор

Существа ищут еду, тратят энергию на жизнь, набравшие достаточно — делятся надвое. Приспособленность нигде не записана формулой: больше потомков оставляет тот, кто съел больше. Всё остальное — следствие.

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

Поимка вероятностная: шанс растёт с превосходством в размере, промах возможен всегда и стоит сил. Если шанс ниже 5%, охота вообще не начинается — иначе существа непрерывно бросаются на почти равных и жгут энергию впустую.

Ещё существа стареют: опасность гибели растёт с возрастом и удваивается за 1200 тактов. Смерти «от старости» как события нет — есть накопленная ветхость, из-за которой любая случайность всё вероятнее.

Детство

Размер в генах — это размер взрослого. Рождается существо вдвое меньше и дорастает за свой счёт: тело строится постепенно, из съеденного. Пока не дорос — ты дешевле в содержании, но у тебя короче захват, ты не можешь размножаться, и съесть тебя может почти кто угодно.

Мир неоднороден

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

Половое размножение

Переключатель в разделе «Размножение». При половом нужен взрослый партнёр в пределах чутья, а гены потомка собираются из двух наборов одноточечным кроссинговером: до случайной точки признаки берутся у одного родителя, после — у другого. Так возникает сцепление — соседние гены чаще путешествуют вместе, чем расходятся.

Цену секса при этом видно прямо в панели: счётчик «роды сорвались без пары» за прогон набирает сотни тысяч. Партнёра надо ещё найти.

Поведение тоже эволюционирует

Раньше поведение было жёстким правилом, которое написал автор: сначала спасайся, потом охоться, потом ешь. Теперь правила нет. Существо каждый такт оценивает все доступные соблазны — трава, добыча, спасение, — умножает выгоду каждого на свой наследуемый вес и идёт туда, где вышло больше.

Признак характера Что значит
Азарт во сколько раз добыча заманчивее травы
Страх во сколько существо оценивает собственную шкуру

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

Но саму формулу сравнения всё равно написал автор. Поэтому есть второй режим — переключатель «Чем думает». В режиме «нейросетью» формулы нет вовсе: куда повернуть, решает наследуемая сеть из 22 весов. На входе — три тройки «насколько близко и в какой стороне» (трава, угроза, добыча), собственная сытость и смещение; на выходе — вектор направления в системе отсчёта самого существа.

Скрытого слоя намеренно нет: с ним весов было бы под сотню, а популяция тут около сотни особей с редкими мутациями — такую сеть отбор не успеет нащупать. Линейный слой при этом далеко не пуст: каждый источник входит парой «синус-косинус», так что веса задают матрицу 2×2 — она умеет притягивать, отталкивать и заворачивать вбок, то есть кружить вокруг цели.

Сеть начинается со случайных чисел, никакого поведения в неё не заложено. Работает она не хуже формулы: население 111–193 против 74–236, и при этом даёт самый широкий размах размеров (0,49–2,23) и больше всего охоты — до 31% энергии.

Опасность спадает с расстоянием круче, чем выгода от еды (1/d² против 1/d): хищник в двух шагах — это сейчас, а хищник на другом краю поля — почти ничего. При одинаковом законе спадания страх дорастал до вечной паники, и популяция вымирала от голода посреди поля, полного травы.

Три признака отвечают на вопрос «как добыть еду»:

Признак Что даёт Чем платит
Скорость быстрее добираешься до еды и убегаешь от погони квадрат скорости
Размер можешь охотиться, и тебя труднее съесть содержание тела и, главное, перевозка собственной массы
Чутьё раньше видишь и еду, и опасность линейно

Расход за такт складывается из пяти слагаемых, и каждое нужно:

BASE  +  размер³  +  скорость²  +  размер³ × скорость²  +  чутьё
 │         │            │                 │                  │
 │         │            │                 │                  └ держать чувства открытыми
 │         │            │                 └ разгонять и тормозить массу
 │         │            └ само движение: координация, трение
 │         └ содержать тело
 └ обмен веществ: платишь, даже если лежишь

Четвёртое слагаемое — то самое «крупный и быстрый разоряется». Но связка не может быть единственной платой за скорость: при усыхании множитель размер³ обнулял бы и цену скорости, и та уезжала бы в потолок (проверено — так и было). Поэтому рядом стоит независимое скорость², которое не исчезает ни при каком размере.

Деление тоже не бесплатно: потомку надо построить тело, и стоит оно как объём (BUILD_COST × размер³). Не потянул — деления не будет, придётся ещё копить. Раньше здесь был плоский процент, и это скрывало дыру: потомок наследовал размер родителя, но за само тело никто не платил, только за содержание. Крупным быть выходило дешевле, чем должно.

Мутации редкие: чаще всего потомок — точная копия, но с вероятностью 12% один из признаков заметно съезжает. Раньше при каждом рождении менялись все признаки понемногу — популяция была гладким облаком, плавно стекающим в оптимум, и эволюция превращалась в градиентный спуск.

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

Ещё два признака про другое — «что с добытым делать»:

Признак Что даёт Чем платит
Порог деления делишься поздно — потомки крепкие делишься редко, можно не дожить
Доля потомку отдал больше — потомок живучее остался ни с чем сам

Им не нужна выдуманная цена, размен возникает сам. Но порог деления работает только потому, что в мире есть случайная гибель (ACCIDENT) — смерть не от голода, а просто так. Без неё копить энергию было бы бесплатно и совершенно безопасно, тянуть с делением всегда выгодно, и порог уезжал бы в потолок (проверено: за 300 000 тактов доходил до 4,3 из максимума 5). Смертность, не зависящая от сытости, — это то, из-за чего откладывать потомство становится ставкой.

Доля потомку интересна тем, что пока она ровно ½, это не размножение, а деление: две одинаковые половинки, родителя и потомка не различить. Как только она перестаёт быть ½, появляется асимметрия — один отдаёт, другой получает. Та самая граница, которую жизнь однажды перешла: желток в яйце, молоко, забота о потомстве.

Все начинают одинаковыми (по 1,0, доля потомку ½), разнообразие появляется само из мутаций при делении. Вот к чему приходит мир за 600 000 тактов:

Признак Куда пришёл
Размер 1,3 – 1,5, отдельные особи до 2,2
Скорость 1,3 – 1,7
Чутьё 0,8 – 1,2
Порог деления 1,8 – 3,5
Доля потомку 0,45 – 0,7
Азарт 0,05 – 1,0
Страх сильно гуляет, от 1 до 15

Население 114–127 в трёх прогонах по 600–800 тысяч тактов. Энергии популяция берёт примерно 93% с травы и 7% с охоты.

Охота втрое увеличила размер тела — до неё он держался на 0,52. Заодно упала скорость: когда еды вдоволь, гнаться наперегонки незачем.

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

Порог деления пришлось выпустить за прежний потолок 5 — он в него упирался. Правильный ход при упоре в границу: поднять границу и посмотреть, где признак остановится сам, а не принимать упор за равновесие.

Расслоения на два вида, впрочем, не вышло. Распределение размеров осталось одногорбым с длинным правым хвостом: не «хищники и жертвы», а единая популяция, где крупные едят мелких из своего же числа. В природе так и живут многие рыбы.

Доля потомку блуждает сильнее всех признаков и в среднем держится ниже ½ — родитель обычно оставляет себе больше половины. Похоже, отбор давит на неё слабо: вокруг ½ приспособленность почти не меняется, и решает уже дрейф. Получается, дрейф видно прямо внутри мира с отбором — на жёлтом графике.

Что выяснилось по дороге

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

Охота должна быть намеренной

Самая поучительная ошибка. Поимка срабатывала от одного касания: мирно пасущийся взрослый съедал подвернувшегося младенца, вовсе не собираясь охотиться. Пока все рождались сразу взрослыми, это почти не было заметно. Стоило добавить детство — и популяция начала выедать собственный молодняк, вымирая за 20–50 тысяч тактов.

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

Побочный вывод, стоящий отдельного упоминания: до этой правки режим «нейросетью» выглядел успешнее формулы — население 109–118 против вымирания. Оказалось, сеть просто кормилась дырой: 39% её энергии шло от случайного поедания младенцев. Как только охота стала намеренной, эта версия сети умерла за тысячу тактов. Вывод «нейросеть работает лучше» был артефактом бага, и без проверки он бы так и остался в выводах.

Медленный рост убивает вернее, чем неоднородность

Когда детство только появилось, мир начал вымирать, и первым подозреваемым были пятна травы: казалось, скученность у кормушек ведёт к взаимному истреблению. Разделение показало обратное — виноват был слишком медленный рост.

Что менялось 300 000 тактов, 2 прогона
Пятна выключены, детства нет 104 и 118
Пятна включены, детства нет вымирание ×2 (при медленном росте)
Детство есть, рост 0,004 вымирание ×2
Детство есть, рост 0,02 135 и 89
То же плюс 4 крупных пятна 127 и 156
То же плюс 20 мелких пятен 108 и 153

При росте 0,004 путь до взрослости занимал 137 тактов при средней жизни втрое короче — большинство просто не доживало до размножения. С ускоренным ростом неоднородность перестала быть проблемой и заработала как задумано: при крупных пятнах размах взрослых размеров вырос до 0,32–2,50, а доля энергии от охоты — до 22–28%.

Охота не должна создавать энергию из ничего

Самая дорогая ошибка за всё время настройки. Питательность добычи (meat) стояла 30 за единицу объёма, а постройка того же тела стоила 6. Разницу мир получал из ниоткуда: охота работала вечным двигателем. Внешне это выглядело не как изобилие, а как необъяснимое вымирание — популяция раскачивалась и гибла на 50–500 тысячах тактов, и никакая подстройка травы не помогала.

Питательность добычи Что вышло за 600 000 тактов (2 прогона)
0 население 127 и 115
8 32 и вымирание
15 вымирание и 88
30 вымирание в обоих
30, но шанс поимки 20% 113 и 136

Отсюда правило: meat имеет смысл сравнивать не с питательностью травы, а со стоимостью постройки тела. По умолчанию ноль — победителю достаётся только запас жертвы, а энергия, вложенная когда-то в её тело, считается рассеявшейся. Охота при этом никуда не девается: за прогон случается 50–70 тысяч удачных нападений, просто она перестала быть источником дармовой энергии.

Последняя строка таблицы показывает, что дело не в самой добыче, а в общем потоке: при том же meat = 30, но вчетверо меньшем шансе поимки мир снова живёт.

Хищничеству нужна кормовая база

Самое неочевидное в настройке: охота работает только при изобилии травы. Пришлось поднять прирост еды с 2,8 до 6 за такт. Вот что даёт перебор на 300 000 тактов:

Еды за такт Население Размер Съедено сородичей
2,8 56 – 87 0,64 4 900
6 105 – 141 ~1,0 18 400
10 68 – 173, шатает 1,4 – 1,9 27 700

При скудной траве хищничество почти не заводится, а если его усилить насильно — население падает до 15–30 и мир балансирует на грани вымирания. Логика та же, что в настоящей экологии: хищников кормит не добыча, а трава под добычей.

Справа от поля — летопись: население и средние значения признаков. По ней отбор и виден. Она никогда не обрезает начало прогона: когда буфер заполняется, точки прореживаются вдвое и запись идёт вдвое реже, поэтому весь путь от нулевого такта виден целиком при любой длине прогона.

На поле цвет означает скорость (голубые медленные, оранжевые быстрые), радиус круга — размер, бледное кольцо — чутьё.

Параметры крутятся на ходу

Всё устройство мира — 23 ползунка в панели справа: прирост и питательность травы, цена каждого слагаемого расхода, смертность, старение, сила мутаций, выгода и риск охоты, размеры мира. Меняются прямо во время прогона; те немногие, что перестраивают мир целиком, применяются по отпусканию ползунка. Кнопка внизу возвращает всё к исходному.

Что попробовать в первую очередь: прирост травы. От изобилия зависит и население, и то, окупается ли охота, и во что выгодно вкладывать тело. И Цена собственной жизни — при нуле бегать перестанут вовсе.

Насколько это честная модель отбора

Честная в механизме, произвольная в содержании.

Что настоящее. Приспособленности нигде нет — нет строчки fitness = f(гены), есть энергия, голод и смерть, а кто оставит больше потомков, выясняется по ходу. Мутации ненаправленные. Отбор зависит от плотности и состава популяции: выгодный размер определяется тем, кто вокруг. Дрейф не подавлен и виден.

И один настоящий признак адекватности: модель воспроизвела предсказание, которого в неё не закладывали. Из r/K-теории следует, что чем выше внешняя смертность, тем раньше надо размножаться. В переборе по ACCIDENT вышло ровно это: 0,0003 → порог ~2,5; 0,0006 → 1,5–2,6; 0,0015 → ~1,3.

Что выдумано. Формула расхода и все её коэффициенты подобраны руками — и подобраны так, чтобы картинка получилась интересной, без упоров в границы. Значит, «скорость выросла, размер упал» — это не открытие про эволюцию, а прочтение вслух моих коэффициентов.

Чего структурно нет.

  • Сеть без скрытого слоя. Линейный слой выразителен, но всё же не любое поведение. Со скрытым слоем весов было бы под сотню, и популяции в сотню особей их не нащупать — нужен мир на тысячи существ, а это упирается в скорость.
  • Сцепление есть, но геном плоский. Кроссинговер одноточечный, порядок генов задан руками и не эволюционирует. Ни хромосом, ни диплоидности, ни доминирования.
  • Пятна травы — единственная неоднородность. Нет ни времён года, ни рельефа, ни разных типов пищи.
  • Расхождения на виды по-прежнему нет. Размах размеров вырос (0,3–2,5), но распределение остаётся одногорбым: единая популяция, где крупные едят мелких из своего же числа.

Короткий итог: как инструмент для интуиции («что отбор делает с разменом») работает. Как предсказание — нет, потому что предсказывает свойства формулы, которую мы сами и написали.

Из чего состоит

Файл Что делает
src/worlds/ Миры. Всё содержательное живёт здесь.
src/worlds/index.ts Что такое мир, список готовых и список запланированных.
src/worlds/selection.ts Естественный отбор.
src/worlds/empty.ts Пустой мир. Образец, с которого удобно списывать новый.
src/app.ts Выпадающий список, кнопки, счётчик шагов, цикл времени, холст. Про модель не знает ничего.
src/styles.css Внешний вид.

Мир — это объект с четырьмя функциями, app.ts вызывает только их:

create()             // каким мир рождается
step(state)          // что происходит за один шаг времени
draw(ctx, state, …)  // как это выглядит
stats(state)         // какие числа показать в панели справа

Как добавить мир

Скопировать empty.ts, поменять id, title и содержимое четырёх функций, после чего дописать одну строку в src/worlds/index.ts:

export const WORLDS: World[] = [selectionWorld, emptyWorld, мойМир]

Он сразу появится в выпадающем списке. Ни app.ts, ни разметку трогать не нужно.

Как проверять модель, не глядя на экран

Настройки мира приходится подбирать, а на глаз этого не сделать: надо знать, не вымирает ли популяция и не упирается ли признак в границу. Для этого удобно прогнать мир вхолостую, без браузера:

npx esbuild harness.ts --bundle --platform=node --format=esm --outfile=harness.mjs && node harness.mjs

где harness.ts — несколько строк: create(), потом step() в цикле, и печать stats() каждые несколько тысяч тактов. Так за секунду проверяется то, что в браузере пришлось бы разглядывать минуты.

План

В выпадающем списке под заголовком «Дальше» перечислено то, что собираемся написать. Выбрать эти пункты пока нельзя — это план на виду. Как только мир написан, он переезжает из PLANNED в WORLDS.

About

Evolution sandbox in the browser: creatures forage, grow, hunt each other and age — with no fitness function written anywhere

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages