Files
h-school/docs/design/ai.md
T
Leonid Pershin c16c34a83c Refactor project structure and enhance AI and simulation components
- Updated project roles to include a new `HSchool.Ai` component, detailing its responsibilities in routing and decision-making.
- Revised the `HSchool.Simulation` component to integrate with the new AI functionalities, improving the interaction between simulation and AI.
- Expanded documentation to reflect the changes in project roles and the introduction of AI features.
- Added tests for the new AI functionalities, ensuring robust pathfinding and decision-making capabilities.
- Updated phase documentation to outline the new stages of school life, focusing on presence and behavior.
2026-08-19 15:57:12 +03:00

27 KiB
Raw Blame History

Жизнь школы

Договорённость на срез после расписания, не текущий код. Расписание: schedule.md. Штат: staffing.md. Люди: people.md. Типы и карта: defs.md. Экран: near-term.md.

Люди начинают ходить и решать. До сих пор человек находился там, где его застало расписание; теперь он приходит утром, идёт коридором в кабинет, может уйти с урока в туалет и вернуться — и всё это видно на карте.

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

Что уже готово

Три вещи не надо изобретать — они лежат с прошлых срезов:

  • Граф ходьбы. MapLayout.Links — рёбра между двором и комнатами, валидатор требует связности. Коридоры, лестницы, вестибюль и двор уже узлы этого графа, а не декорация.
  • Комнаты для нужд. В core есть столовая, два санузла, учительская, библиотека и двор. Карту под этот срез дорабатывать не нужно.
  • Нужды. Сон, голод, туалет и общение существуют с нулевым decayPerHour — потому что восполнить их было нечем.

Присутствие — состояние, а не функция от времени

Ключевая смена модели. Сейчас «кто в кабинете» вычисляется из расписания и часов (TimetableClock); занятость нигде не хранится, и это было правильно, пока никто не ходил.

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

Ходьба

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

Из этого сразу следует то, ради чего всё затевалось: на перемене коридор действительно полон людей, и это видно в дереве.

Цена перехода — свойство дефа локации: travelMinutes на RoomDef и TerritoryDef, столько игровых минут занимает пересечение узла. В core: кабинет 0.5, коридор 1.5, лестница 1.5, вестибюль 1, двор 3. Мод, где двор — это парк на гектар, ставит своё число и получает школу, по которой опаздывают.

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

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

Приход и уход

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

  • Ученик приходит к своему первому уроку и уходит после последнего.
  • Учитель — так же, по своим урокам.
  • Прочий штат (директор, секретарь, повар, медсестра, библиотекарь) работает по дню школы: приходит к первому уроку дня и уходит после последнего.
  • Родители не приходят. Их вызов в школу — отдельный сюжет, не в этом срезе.

Запас времени на дорогу человек берёт себе сам: путь от двора до кабинета плюс несколько минут из своего потока случайности. Аккуратный придёт раньше, ленивый — в притык, и если путь длиннее запаса, он опоздает. Это первое место, где черта характера делает что-то видимое.

Вне школы человек не исчезает: у него состояние «вне школы», он есть в списке людей, и его карточка это показывает. Дом мы не моделируем.

Пропуск пустого времени

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

Два условия, и оба проверяет сервер:

  1. в школе никого нет;
  2. сейчас вне рабочего окна дня — либо нерабочий день целиком, либо рабочий, но до шести утра или после последнего звонка.

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

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

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

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

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

Действия — дефами

ActionDef существует, но пуст. Он получает поля:

{
  "defName": "UseToilet",
  "room": "Restroom",          // где это делают
  "minutes": 4,                // сколько длится
  "need": "Toilet",            // что восполняет
  "needGain": 1,               // насколько, за всё действие
  "roles": ["student", "staff"],
  "weight": 0,                 // 0 — ради удовольствия не выбирают
}

Действие может требовать вещь (thing), и тогда занимает её на время: за столом в столовой не сидят вдвоём. Это связывает мебель на карте с поведением, а не только с вместимостью класса.

Так мод добавляет поведение, не написав ни строчки кода, — ровно как обещано в defs.md.

Обязанности из каталога действий не выбирают. Урок приходит из расписания, работа по должности — из RoomDef.works, который уже проставлен в core (TeachLesson у кабинета, ServeLunch у столовой, MedicalDuty у медкабинета). ActionDef описывает то, что человек делает сам: нужды и досуг.

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

Как выбирается действие

Гибрид: обязанность — не приказ, а сильная цель среди прочих.

В момент решения человек собирает цели и сравнивает их вес:

Цель Вес
Обязанность (урок, работа по должности) постоянный, высокий
Нужда ниже порога тем больше, чем ближе нужда к нулю
Досуг низкий; берётся, когда обязанности нет

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

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

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

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

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

Нужды

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

Прогресс-бары нужд в карточке уже нарисованы. В этом срезе они впервые начнут шевелиться.

Последствия — минимум

Осознанно мало:

  • Навык растёт на уроке — от предмета, с поправкой на черты и на то, в каком человек состоянии. Голодный учится хуже.
  • Опоздание и прогул видны. В кабинете 5Б на математике не двадцать три человека, а двадцать один, и карточка каждого говорит, где он.

Оценок, успеваемости, конфликтов и настроения тут нет. Это следующий срез, и он будет опираться на числа, которые появятся здесь.

Что видит игрок

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

На экране:

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

Поток на клиент

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

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

Имена в поток не кладём: клиент один раз забирает по HTTP короткий справочник «id → имя» и держит его открытым; неизвестный id — повод перечитать справочник, а не строка в каждом кадре.

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

Отдельная библиотека

HSchool.Ai встаёт рядом с People и Schedule: зависит на Content, People и Schedule, не знает ни про Arch, ни про ASP.NET, ни про часы реального времени.

Граница проходит там, где мало трафика. В библиотеку уезжает всё, что считается функцией от входов:

  • маршруты — граф из MapLayout.Links и travelMinutes в матрицу следующих шагов и стоимость пути;
  • план дня — во сколько человеку выходить, чтобы успеть, и когда он уходит;
  • решение — цели, веса, выбор действия и цепочка «дойти → сделать»;
  • продвижение плана — прошло столько-то минут, значит человек здесь и делает вот это.

В HSchool.Simulation остаётся то, что без мира не имеет смысла: компоненты, порядок обхода, тик и адаптер, который собирает значения из компонентов, зовёт библиотеку и записывает ответ обратно.

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

Взамен — то же, что уже дали People и Schedule: самая сложная логика базы проверяется таблицей входов и выходов, без ECS, без сокета и без хоста. Для среза, где половина вопросов будет вида «почему он пошёл туда», это и есть главный аргумент.

Детерминизм

Тот же сид и те же действия игрока дают ту же школу. Это стоит дороже, чем кажется, и держится на трёх вещах:

  1. Стабильный порядок обхода. Порядок сущностей в Arch меняется при создании и удалении — опираться на него нельзя. Обход идёт по собственному упорядоченному списку людей.
  2. Свой поток случайности у каждого. Как в генерации: Seed.Mix от сида школы, номера человека, соли и номера дня. Никакого общего Random на мир.
  3. Решения по событиям, а не по бюджету времени. Ограничение «столько решений за тик» — очередь, а не отсечка: отложенное решение принимается следующим тиком, а не пропадает.

Сохранение

Место человека, его путь и текущее действие едут в файл часов — там же, где уже лежат карта и моды, с той же редкой периодичностью и обязательным сбросом при остановке. Кого в файле нет (новичок после набора), тот расставляется по обязанности.

Отдельного формата под ИИ не заводим: тридцать секунд неточности после аварийной остановки игрок не заметит, а лишний файл на школу — заметим мы.

Масштаб

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

Потолок решений за тик — настройка движка, ему место в SimulationOptions.

Что зафиксировано

Тема Решение
Этапы Два: присутствие и ходьба, затем нужды и решения
Присутствие Состояние человека, а не функция от расписания
Ходьба Всегда в узле; переход — занятие узлов пути
Цена перехода travelMinutes в дефе локации
Маршруты Матрица следующих шагов, один раз при загрузке
Приход Только если сегодня есть уроки; запас на дорогу свой у каждого
Прочий штат По дню школы, а не по своим урокам
Родители Не приходят
Вне школы Состояние, не исчезновение; дом не моделируем
Пропуск пустого времени Кнопка к шести утра ближайшего рабочего дня: ночь, выходной, каникулы
Условие кнопки Пустая школа и время вне рабочего окна дня; решает сервер, шлёт с часами
Границы окна По каркасу дня, а не по фактическому расписанию — иначе прыгали бы через дни
Сон Восстанавливается за кадром
Действия Дефами: комната, вещь, длительность, нужда, роли, вес
Обязанности Из расписания и RoomDef.works, не из каталога действий
Выбор Обязанность — сильная цель, а не приказ; побеждает наибольший вес
Планирование Вырожденный GOAP: «дойти → сделать», два предусловия
Переключение Начатое доводится до конца, если новая цель не сильно тяжелее
Частота решений По событиям: звонок, конец действия, приход, порог нужды
Нужды Декей включается; сон вне школы
Последствия Навык на уроке, видимые опоздание и прогул. Оценок нет
Игрок Наблюдает; приказов нет
Поток присутствия Своё сообщение, ~2 Гц, по всей школе, фильтр на клиенте
Снимок карты Становится статическим: всё живое уходит в присутствие
Имена Справочник по HTTP, не в каждом кадре
Библиотека HSchool.Ai рядом с People и Schedule; в Simulation — только адаптер
Детерминизм Держим: свой порядок обхода, поток на человека, очередь решений
Сейв В файле часов, вместе с картой и модами

Заведомо не сейчас

  • Оценки, успеваемость, настроение, конфликты и дружба.
  • Приказы игрока и вызов родителей в школу.
  • Ночная школа, сон как событие, продлёнка.
  • Пиксельные координаты внутри комнаты и рисованные человечки.
  • Полноценный GOAP с поиском по пространству состояний.
  • Серверный OpenLocation — пока фильтрует клиент.