- Revised `AGENTS.md` to specify that the same seed, map, name set, and native language must produce the same roster in tests. - Updated `protocol.md` to clarify that everyone in the school is generated with the selected native language, and related languages may appear at a low skill. - Enhanced `ai.md` to explain skill growth mechanics based on state and the introduction of minimum ranges for skills not yet acquired. - Improved `people.md` to detail the generation of skills and traits, emphasizing that not every person receives every skill and the implications of body attributes on skill acquisition. - Adjusted `projects.md` to reflect the inclusion of native language in roster generation, ensuring comprehensive documentation of project components.
28 KiB
Жизнь школы
Договорённость на срез после расписания, не текущий код.
Расписание: 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 — летние каникулы в таком темпе это несколько минут пустого экрана, а каждая ночь — полминуты. Поэтому: кнопка «пропустить», переносящая время к шести утра ближайшего рабочего дня.
Два условия, и оба проверяет сервер:
- в школе никого нет;
- сейчас вне рабочего окна дня — либо нерабочий день целиком, либо рабочий, но до шести утра или после последнего звонка.
Первое выглядит как вежливость к игроку, но дело не в ней: оно и делает прыжок законным. Вне школы нужды не тратятся, а сон восстанавливается за кадром — значит для пустой школы промотанное время и прожитое дают ровно одно состояние. Останься в здании хоть один человек, прыжок пришлось бы либо честно просимулировать, либо соврать.
Что прыжок обязан отработать по-настоящему — это события календаря, через которые он перескакивает: первое сентября (переход, выпуск, набор) и недельное обновление пула соискателей. Оба уже написаны как «что случилось между двумя датами», а не «что случилось на этом тике», так что прыжок их не ломает. Но тест обязателен: летние каникулы перескакивают ровно через первое сентября, и это самый вероятный способ потерять целый набор.
Разрешение на кнопку приходит вместе с часами. Клиент не вычисляет его сам — это была бы игровая логика на клиенте, а её там нет.
Шесть утра — то же время, с которого начинается новая школа: до первого звонка есть чем заняться. В три часа ночи вторника прыжок попадает в утро того же вторника, в десять вечера — в утро среды, в субботу днём — в утро понедельника. Правило одно: ближайшее шесть утра рабочего дня, которое ещё впереди.
Границы рабочего окна берутся из каркаса дня, а не из фактического расписания. Разница важна: у школы без единого нанятого учителя уроков нет вообще, и по фактическому расписанию кнопка позволила бы перепрыгивать целые учебные дни. По звонкам — не позволит.
Действия — дефами
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: это его вырожденный случай. Полноценный поиск по пространству состояний в школе не окупается — он стоит заметно дороже и не порождает ни одной новой истории сверх «дойти и сделать». Если однажды появятся действия, которые открывают другие действия (взять ключ → открыть кабинет → принести журнал), планировщик углубится, а форма целей и предусловий останется той же. Место для роста заложено, цена — нет.
Одно правило против дёрганья: начатое доводят до конца. Решение переключается, только если новая цель весит заметно больше текущей. Иначе человек с двумя почти равными нуждами будет метаться между столовой и туалетом, ничего не успевая.
Решение принимается не каждый тик, а по событиям: звонок, конец действия, приход в назначенный узел, пересечение нуждой порога. Двадцать раз в секунду думать не о чем.
Нужды
Декей включается. Голод — столовая, туалет — санузел, общение — перемена рядом с одноклассниками. Сон за кадром: человек вне школы отдыхает, и утром приходит выспавшимся. Ночной школы у нас нет, а изображать её ради одной полоски — лишняя работа.
Прогресс-бары нужд в карточке уже нарисованы. В этом срезе они впервые начнут шевелиться.
Последствия — минимум
Осознанно мало:
- Навык растёт на уроке — от предмета, с поправкой на черты и на то, в каком человек
состоянии. Голодный учится хуже. Если ключа ещё нет (химия в восьмом), рост начинается с
Range.Min, а не с нуля «как будто навык был всегда». - Опоздание и прогул видны. В кабинете 5Б на математике не двадцать три человека, а двадцать один, и карточка каждого говорит, где он.
Оценок, успеваемости, конфликтов и настроения тут нет. Это следующий срез, и он будет опираться на числа, которые появятся здесь.
Что видит игрок
Игрок пока наблюдает. Приказы — «вызвать к директору», «отправить домой» — отдельный разговор; сначала школа должна жить без вмешательства.
На экране:
- дерево — сколько человек в каждом узле, живым числом;
- панель локации — кто здесь и чем занят;
- карточка человека — где он сейчас и что делает, рядом с уже существующим расписанием.
Поток на клиент
Присутствие меняется постоянно, поэтому у него своё сообщение — примерно два раза в секунду, только по открытой школе. Полсекунды задержки на счётчик в дереве незаметны, а двадцать кадров в секунду ради этого не нужны.
Побочный выигрыш: снимок карты становится статическим. Сейчас он несёт и людей, и текущий урок, и поэтому пересылается на каждой смене слота. Всё живое уезжает в присутствие, структура уходит один раз при открытии — и одна из самых неприятных зависимостей в протоколе исчезает.
Имена в поток не кладём: клиент один раз забирает по HTTP короткий справочник «id → имя» и держит его открытым; неизвестный id — повод перечитать справочник, а не строка в каждом кадре.
Присутствие приходит по всей школе, а выбранный узел клиент фильтрует сам — как дерево
локаций сейчас. Если школа на тысячу человек однажды сделает это дорогим, серверный
OpenLocation — тот самый шов, который для этого оставлен.
Отдельная библиотека
HSchool.Ai встаёт рядом с People и Schedule: зависит на Content, People и Schedule,
не знает ни про Arch, ни про ASP.NET, ни про часы реального времени.
Граница проходит там, где мало трафика. В библиотеку уезжает всё, что считается функцией от входов:
- маршруты — граф из
MapLayout.LinksиtravelMinutesв матрицу следующих шагов и стоимость пути; - план дня — во сколько человеку выходить, чтобы успеть, и когда он уходит;
- решение — цели, веса, выбор действия и цепочка «дойти → сделать»;
- продвижение плана — прошло столько-то минут, значит человек здесь и делает вот это.
В HSchool.Simulation остаётся то, что без мира не имеет смысла: компоненты, порядок обхода,
тик и адаптер, который собирает значения из компонентов, зовёт библиотеку и записывает ответ
обратно.
Адаптер — настоящая цена разделения: на каждое решение надо сложить нужды и роль человека в структуру и разобрать ответ. Цена приемлема ровно потому, что решения редки, а движение часто: горячий путь — уменьшить остаток минут и перейти в следующий узел — границу не пересекает вовсе, а думает человек несколько раз за урок. Провести черту где-нибудь ещё означало бы гонять копирование двадцать раз в секунду на каждого.
Взамен — то же, что уже дали People и Schedule: самая сложная логика базы проверяется
таблицей входов и выходов, без ECS, без сокета и без хоста. Для среза, где половина вопросов
будет вида «почему он пошёл туда», это и есть главный аргумент.
Детерминизм
Тот же сид и те же действия игрока дают ту же школу. Это стоит дороже, чем кажется, и держится на трёх вещах:
- Стабильный порядок обхода. Порядок сущностей в Arch меняется при создании и удалении — опираться на него нельзя. Обход идёт по собственному упорядоченному списку людей.
- Свой поток случайности у каждого. Как в генерации:
Seed.Mixот сида школы, номера человека, соли и номера дня. Никакого общегоRandomна мир. - Решения по событиям, а не по бюджету времени. Ограничение «столько решений за тик» — очередь, а не отсечка: отложенное решение принимается следующим тиком, а не пропадает.
Сохранение
Место человека, его путь и текущее действие едут в файл часов — там же, где уже лежат карта и моды, с той же редкой периодичностью и обязательным сбросом при остановке. Кого в файле нет (новичок после набора), тот расставляется по обязанности.
Отдельного формата под ИИ не заводим: тридцать секунд неточности после аварийной остановки игрок не заметит, а лишний файл на школу — заметим мы.
Масштаб
Считаем на нынешние сотни людей, а не сразу на тысячу. Дорогих мест два, и оба уже закрыты формой решения: маршруты считаются один раз при загрузке, решения принимаются по событиям. Движение — это уменьшение счётчика у тех, кто идёт.
Потолок решений за тик — настройка движка, ему место в SimulationOptions.
Что зафиксировано
| Тема | Решение |
|---|---|
| Этапы | Два: присутствие и ходьба, затем нужды и решения |
| Присутствие | Состояние человека, а не функция от расписания |
| Ходьба | Всегда в узле; переход — занятие узлов пути |
| Цена перехода | travelMinutes в дефе локации |
| Маршруты | Матрица следующих шагов, один раз при загрузке |
| Приход | Только если сегодня есть уроки; запас на дорогу свой у каждого |
| Прочий штат | По дню школы, а не по своим урокам |
| Родители | Не приходят |
| Вне школы | Состояние, не исчезновение; дом не моделируем |
| Пропуск пустого времени | Кнопка к шести утра ближайшего рабочего дня: ночь, выходной, каникулы |
| Условие кнопки | Пустая школа и время вне рабочего окна дня; решает сервер, шлёт с часами |
| Границы окна | По каркасу дня, а не по фактическому расписанию — иначе прыгали бы через дни |
| Сон | Восстанавливается за кадром |
| Действия | Дефами: комната, вещь, длительность, нужда, роли, вес |
| Обязанности | Из расписания и RoomDef.works, не из каталога действий |
| Выбор | Обязанность — сильная цель, а не приказ; побеждает наибольший вес |
| Планирование | Вырожденный GOAP: «дойти → сделать», два предусловия |
| Переключение | Начатое доводится до конца, если новая цель не сильно тяжелее |
| Частота решений | По событиям: звонок, конец действия, приход, порог нужды |
| Нужды | Декей включается; сон вне школы |
| Последствия | Навык на уроке, видимые опоздание и прогул. Оценок нет |
| Игрок | Наблюдает; приказов нет |
| Поток присутствия | Своё сообщение, ~2 Гц, по всей школе, фильтр на клиенте |
| Снимок карты | Становится статическим: всё живое уходит в присутствие |
| Имена | Справочник по HTTP, не в каждом кадре |
| Библиотека | HSchool.Ai рядом с People и Schedule; в Simulation — только адаптер |
| Детерминизм | Держим: свой порядок обхода, поток на человека, очередь решений |
| Сейв | В файле часов, вместе с картой и модами |
Заведомо не сейчас
- Оценки, успеваемость, настроение, конфликты и дружба.
- Приказы игрока и вызов родителей в школу.
- Ночная школа, сон как событие, продлёнка.
- Пиксельные координаты внутри комнаты и рисованные человечки.
- Полноценный GOAP с поиском по пространству состояний.
- Серверный
OpenLocation— пока фильтрует клиент.