# Жизнь школы Договорённость на срез после расписания, не текущий код. Расписание: [`schedule.md`](../04-schedule/schedule.md). Штат: [`staffing.md`](../03-staffing/staffing.md). Люди: [`people.md`](../02-people/people.md). Типы и карта: [`defs.md`](../01-shell/defs.md). Экран: [`near-term.md`](../01-shell/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` существует, но пуст. Он получает поля: ```jsonc { "defName": "UseToilet", "room": "Restroom", // где это делают "minutes": 4, // сколько длится "need": "Toilet", // что восполняет "needGain": 1, // насколько, за всё действие "roles": ["student", "staff"], "weight": 0, // 0 — ради удовольствия не выбирают } ``` Действие может требовать **вещь** (`thing`), и тогда занимает её на время: за столом в столовой не сидят вдвоём. Это связывает мебель на карте с поведением, а не только с вместимостью класса. Так мод добавляет поведение, не написав ни строчки кода, — ровно как обещано в [`defs.md`](../01-shell/defs.md). **Обязанности из каталога действий не выбирают.** Урок приходит из расписания, работа по должности — из `RoomDef.works`, который уже проставлен в `core` (`TeachLesson` у кабинета, `ServeLunch` у столовой, `MedicalDuty` у медкабинета). `ActionDef` описывает то, что человек делает **сам**: нужды и досуг. Числа поведения — отдельный деф правил, как `StaffingDef` у штата: пороги нужд, скорость обучения на уроке, разброс запаса на дорогу, порог переключения решения и веса целей. Это игровой баланс, а не настройка движка, и место ему в контенте. ## Как выбирается действие Гибрид: обязанность — не приказ, а **сильная цель среди прочих**. В момент решения человек собирает цели и сравнивает их вес: | Цель | Вес | | --- | --- | | Обязанность (урок, работа по должности) | постоянный, высокий | | Нужда ниже порога | тем больше, чем ближе нужда к нулю | | Досуг | низкий; берётся, когда обязанности нет | Побеждает наибольший. Отсюда бесплатно получается то, ради чего гибрид и нужен: ученик с критическим «туалетом» уходит с урока, потому что нужда **обогнала** обязанность, а не потому что где-то записано «можно выйти». Дальше — планирование. Цель выбирает действие, которое её закрывает, а планировщик достраивает недостающее: «дойти до узла с такой комнатой» → «сделать». Предусловия у нас всего два — быть в подходящей комнате и иметь свободную вещь, — поэтому цепочка выходит длиной в два-три шага. Честно про GOAP: **это его вырожденный случай**. Полноценный поиск по пространству состояний в школе не окупается — он стоит заметно дороже и не порождает ни одной новой истории сверх «дойти и сделать». Если однажды появятся действия, которые открывают другие действия (взять ключ → открыть кабинет → принести журнал), планировщик углубится, а форма целей и предусловий останется той же. Место для роста заложено, цена — нет. Одно правило против дёрганья: **начатое доводят до конца.** Решение переключается, только если новая цель весит заметно больше текущей. Иначе человек с двумя почти равными нуждами будет метаться между столовой и туалетом, ничего не успевая. Решение принимается **не каждый тик**, а по событиям: звонок, конец действия, приход в назначенный узел, пересечение нуждой порога. Двадцать раз в секунду думать не о чем. ## Нужды Декей включается. Голод — столовая, туалет — санузел, общение — перемена рядом с одноклассниками. Сон **за кадром**: человек вне школы отдыхает, и утром приходит выспавшимся. Ночной школы у нас нет, а изображать её ради одной полоски — лишняя работа. **Голод восстанавливается там же**: дома завтракают и ужинают, иначе дефицит копится из дня в день и вся школа через неделю сидит на нуле — так и было, пока это не измерили. **Обед — расписание, а не порыв.** Действие с `lunch: true` предлагается только в свою смену (`DayFrameDef.lunchBreaks`), зато в неё идут и те, кто ещё не проголодался до порога. Иначе получается одно из двух: либо класс уходит с урока в столовую, когда приспичило, либо — если ждать порога — вся параллель проскакивает свою смену и голодает до вечера. Смены разводят параллели: в `core` младшие едят после третьего урока, старшие после четвёртого, и каждая такая перемена становится длинной. Прогресс-бары нужд в карточке уже нарисованы. В этом срезе они впервые начнут шевелиться. ## Последствия — минимум Осознанно мало: - **Навык растёт на уроке** — от предмета, с поправкой на черты и на то, в каком человек состоянии. Голодный учится хуже. Если ключа ещё нет (химия в восьмом), рост начинается с `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, без сокета и без хоста. Для среза, где половина вопросов будет вида «почему он пошёл туда», это и есть главный аргумент. ## Детерминизм Тот же сид и те же действия игрока дают ту же школу. Это стоит дороже, чем кажется, и держится на трёх вещах: 1. **Стабильный порядок обхода.** Порядок сущностей в Arch меняется при создании и удалении — опираться на него нельзя. Обход идёт по собственному упорядоченному списку людей. 2. **Свой поток случайности у каждого.** Как в генерации: `Seed.Mix` от сида школы, номера человека, соли и номера дня. Никакого общего `Random` на мир. 3. **Решения по событиям, а не по бюджету времени.** Ограничение «столько решений за тик» — очередь, а не отсечка: отложенное решение принимается следующим тиком, а не пропадает. ## Сохранение Место человека, его путь и текущее действие едут в файл часов — там же, где уже лежат карта и моды, с той же редкой периодичностью и обязательным сбросом при остановке. Кого в файле нет (новичок после набора), тот расставляется по обязанности. Отдельного формата под ИИ не заводим: тридцать секунд неточности после аварийной остановки игрок не заметит, а лишний файл на школу — заметим мы. ## Масштаб Считаем на нынешние сотни людей, а не сразу на тысячу. Дорогих мест два, и оба уже закрыты формой решения: маршруты считаются один раз при загрузке, решения принимаются по событиям. Движение — это уменьшение счётчика у тех, кто идёт. Потолок решений за тик — настройка движка, ему место в `SimulationOptions`. ## Что зафиксировано | Тема | Решение | | --- | --- | | Этапы | Два: присутствие и ходьба, затем нужды и решения | | Присутствие | Состояние человека, а не функция от расписания | | Ходьба | Всегда в узле; переход — занятие узлов пути | | Цена перехода | `travelMinutes` в дефе локации | | Маршруты | Матрица следующих шагов, один раз при загрузке | | Приход | Только если сегодня есть уроки; запас на дорогу свой у каждого | | Прочий штат | По дню школы, а не по своим урокам | | Родители | Не приходят | | Вне школы | Состояние, не исчезновение; дом не моделируем | | Пропуск пустого времени | Кнопка к шести утра ближайшего рабочего дня: ночь, выходной, каникулы | | Условие кнопки | Пустая школа **и** время вне рабочего окна дня; решает сервер, шлёт с часами | | Границы окна | По каркасу дня, а не по фактическому расписанию — иначе прыгали бы через дни | | Сон | Восстанавливается за кадром | | Действия | Дефами: комната, вещь, длительность, нужда, роли, вес | | Обязанности | Из расписания и `RoomDef.works`, не из каталога действий | | Выбор | Обязанность — сильная цель, а не приказ; побеждает наибольший вес | | Планирование | Вырожденный GOAP: «дойти → сделать», два предусловия | | Переключение | Начатое доводится до конца, если новая цель не сильно тяжелее | | Частота решений | По событиям: звонок, конец действия, приход, порог нужды | | Нужды | Декей включается; сон и голод восстанавливаются вне школы | | Обед | Смены по параллелям в каркасе дня; в свою смену едят и не проголодавшиеся | | Последствия | Навык на уроке, видимые опоздание и прогул. Оценок нет | | Игрок | Наблюдает; приказов нет | | Поток присутствия | Своё сообщение, ~2 Гц, по всей школе, фильтр на клиенте | | Снимок карты | Становится статическим: всё живое уходит в присутствие | | Имена | Справочник по HTTP, не в каждом кадре | | Библиотека | `HSchool.Ai` рядом с People и Schedule; в Simulation — только адаптер | | Детерминизм | Держим: свой порядок обхода, поток на человека, очередь решений | | Сейв | В файле часов, вместе с картой и модами | ## Заведомо не сейчас - Оценки, успеваемость, настроение, конфликты и дружба. - Приказы игрока и вызов родителей в школу. - Ночная школа, сон как событие, продлёнка. - Пиксельные координаты внутри комнаты и рисованные человечки. - Полноценный GOAP с поиском по пространству состояний. - Серверный `OpenLocation` — пока фильтрует клиент.