Агенты-грибы жуют воздух, а я навожу порядок в проекте с помощью FSD (AI-Evolution #3)
Доброго утра и добро пожаловать на третью страницу дневника разработки ИИ-песочницы.
Правда, от искусственного интеллекта в проекте пока только развесистые деревья if-else. Разработка каркаса, как оказалось, занимает довольно много сил и времени. В прошлой части я остановился на обнаруженном баге — агенты «играли в чехарду» с кустами, что является неприемлемым поведением. Я узнал, что эта проблема в разработке довольно типична и называется Overshooting.
Эта аномалия вынудила меня форсировать создание инструментов для отладки. Что я сделал? Просто добавил на фронтенд отрисовку «призрака» каждого агента. Напомню: на клиенте картинка обновляется примерно 60 раз в секунду, проще говоря — перед нами привычные 60 кадров. А вот с сервера данные приходят всего 2 раза в секунду (2 Гц). Вся логика рассчитывается как раз по серверным данным. В итоге иногда получалось так: агент стоит перед кустом, он его видит; делает рывок и перепрыгивает; затем замечает его уже сзади, разворачивается — и ситуация повторяется. На самом деле проблема решилась простой арифметикой: я стал высчитывать расстояние от агента до куста. Если это расстояние меньше, чем длина шага агента, то он просто «примагничивается» к кусту. Просто и эффективно.
После этого я заметил еще один неприятный баг в поведении. Несколько агентов с самого начала симуляции стояли как вкопанные. Немного понаблюдав за остальными, я понял закономерность: двое набрасывались на один и тот же куст, и когда тот заканчивался, один агент радостно шел искать следующий, а второй оставался на месте. Я сначала подумал, что не дописал логику поведения для ситуации, когда агент напрыгивает на уже поедаемый куст. В идеале, когда объект уничтожается, у агента должен сбрасываться флаг is_eating, и он отправляется бороздить просторы дальше. Но у второго агента этот сброс почему-то не происходил.
К счастью, на работающем поле проводить дебаг гораздо приятнее. Я написал функцию выбора объекта на поле по клику мыши и вывел его данные на экран. Благодаря этому удалось выяснить, что «агенты-грибы» вечно жевали воздух. Оказалось, когда первый агент доедает куст, еда исчезает из симуляции целиком. Первый сытый агент идет дальше, а у второго проверка сброса состояния не выполняется, потому что его указатель к этому моменту ссылается на уже несуществующий куст. В итоге нужно было просто добавить в его «мозг» дополнительную проверку: «А существует ли еще куст, который я прямо сейчас поглощаю?».
Небольшое отступление: чтобы не душнить сухой разработкой, я попросил ИИ оформить концепцию дебага в виде притчи. Не воспринимайте это всерьез — это скорее техническая байка ради фана, которая наглядно показывает суть оптимизации бэкенда и важность прозрачности системы.
Притча о Жевателях Воздуха и Прозрачной Слюде
В провинции Сунь жил мастер Чен, которому император поручил следить за тремя сотнями механических стражей, охраняющих склады с рисом. Стражи приводились в движение медными пружинами и подчинялись простому правилу, заложенному в их шестерни: видишь мешок — хватай и неси, не видишь ничего — ступай искать дальше.
Вскоре Чен заметил странное: некоторые стражи внезапно замирали посреди двора. Они стояли неподвижно, их железные челюсти ритмично двигались, оглашая окрестности сухим лязгом, но мешков в их руках не было. Чен часами сидел на бамбуковом помосте, наблюдая за ними, и думал: «Наверное, скрытые рычаги в их головах слишком сложны, или духи полуденного зноя путают их помыслы». Наблюдение со стороны не давало ответов, и Чен впал в уныние.
Тогда он отправился к Мудрому Мастеру Ли, жившему у трех рек. Мудрец выслушал Чена, налил чаю и спросил:— Чен, когда ты смотришь на механического стража, видишь ли ты то же самое, что предстает перед его внутренним рычагом?— Нет, — ответил Чен. — Я вижу лишь его внешние движения, а сами шестерни скрыты под глухим медным панцирем.— Твоя беда в том, что ты судишь о помыслах создания по его танцу, но скрываешь от себя его внутреннее устройство, — произнес Ли. — Замени глухую медь на панцири из чистой прозрачной слюды, чтобы каждый винтик был на виду. И начерти Свиток Обязанностей, где будет строго определено место каждой детали, дабы они не путались от сырости.
Чен вернулся в мастерскую. Он убрал тяжелую медь, сделав тела стражей прозрачными, и добавил над головой каждого крошечные разноцветные шелковые ленты, которые поднимались в зависимости от того, какую задачу сейчас исполняет скрытый механизм.
И когда Чен взглянул на двор сквозь это Прозрачное Зеркало, истина открылась ему в одно мгновение.
Оказалось, когда два стража одновременно бежали к одному мешку, первый, будучи быстрее, хватал его и уносил. А второй страж, чей панцирь раньше был скрыт, продолжал верить, что мешок все еще на месте. Внутренний рычаг его ума заклинило в положении «поглощение», хотя самого риса уже не существовало. Бедолага просто жевал пустоту, думая, что исполняет долг. Чен добавил в шестеренки одно маленькое правило: «Прежде чем сомкнуть челюсти, проверь, цел ли мешок», и во дворе воцарился идеальный порядок.
Узнав об этом, философ Чжуан-Цзы записал в свои свитки:
«Безумен тот Творец, кто пытается исправить поведение своих созданий, не видя их внутренних побуждений. Потратить время на наведение Порядка в свитках и создание Прозрачной Слюды — значит обрести глаза. Без них ты подобен слепцу, который пытается направлять глухого, пока тот просто беззвучно жует воздух».
Попутно с отловом «жевателей воздуха» я провел масштабный рефакторинг написанного. Пока все приложение представляло собой прототип, я писал фронтенд-код прямо внутри компонента в useEffect, а на серверной стороне — прямо в main.py. Архитектура представляла собой невнятное месиво.
На бэкенде в Python я пока плохо представляю, как правильно организовывать сложную структуру, поэтому просто навел чистоту. А вот для фронтенда я выбрал методологию FSD (Feature-Sliced Design).
Если говорить просто, FSD — это способ раскладывать код проекта не по «техническим» папкам (когда все компоненты в одной куче, а все функции в другой), а по архитектурным слоям и бизнес-фичам. Если представить мой проект как шкаф с инструментами: поначалу я просто складывал все в одну коробку, но "болтов и гаек" стало слишком много и пришлось создать несколько коробок: «коробка для ремонта крана», «коробка для починки розетки». Внутри каждой фичи-коробки лежит только то, что нужно конкретно ей.
Я вынес всю логику симуляции в отдельный кастомный хук useSimulation. В итоге в самом компоненте с Canvas осталась только разметка самого холста и этот хук, который возвращает ref. Ну и на серверной стороне тоже стало значительно чище.
Еще один небольшой «улучшайзинг» — мне надоело читать красные строчки предупреждений по всему коду. Всё это было терпимо, пока я только набрасывал прототип и проект был крошечным, но в итоге пришлось всё жестко типизировать. Я создал типы и интерфейсы на фронтенде и бэкенде, что значительно упростит навигацию и разработку в будущем.
Следующий шаг — разделение игрового поля на условные клетки для облегчения нагрузки на процессор. Кто шарит - буду рад услышать ваше мнение).
До встречи в следующей части!