333 lines
29 KiB
Markdown
333 lines
29 KiB
Markdown
# Жизнь школы
|
||
|
||
Договорённость на срез после расписания, не текущий код.
|
||
Расписание: [`schedule.md`](schedule.md). Штат: [`staffing.md`](staffing.md).
|
||
Люди: [`people.md`](people.md). Типы и карта: [`defs.md`](defs.md). Экран: [`near-term.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` существует, но пуст. Он получает поля:
|
||
|
||
```jsonc
|
||
{
|
||
"defName": "UseToilet",
|
||
"room": "Restroom", // где это делают
|
||
"minutes": 4, // сколько длится
|
||
"need": "Toilet", // что восполняет
|
||
"needGain": 1, // насколько, за всё действие
|
||
"roles": ["student", "staff"],
|
||
"weight": 0, // 0 — ради удовольствия не выбирают
|
||
}
|
||
```
|
||
|
||
Действие может требовать **вещь** (`thing`), и тогда занимает её на время: за столом в столовой
|
||
не сидят вдвоём. Это связывает мебель на карте с поведением, а не только с вместимостью класса.
|
||
|
||
Так мод добавляет поведение, не написав ни строчки кода, — ровно как обещано в
|
||
[`defs.md`](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` — пока фильтрует клиент.
|