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.
This commit is contained in:
Leonid Pershin
2026-08-19 15:57:12 +03:00
parent 207e7ecdb3
commit c16c34a83c
7 changed files with 578 additions and 2 deletions
+321
View File
@@ -0,0 +1,321 @@
# Жизнь школы
Договорённость на срез после расписания, не текущий код.
Расписание: [`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: **это его вырожденный случай**. Полноценный поиск по пространству состояний в
школе не окупается — он стоит заметно дороже и не порождает ни одной новой истории сверх «дойти
и сделать». Если однажды появятся действия, которые открывают другие действия (взять ключ →
открыть кабинет → принести журнал), планировщик углубится, а форма целей и предусловий останется
той же. Место для роста заложено, цена — нет.
Одно правило против дёрганья: **начатое доводят до конца.** Решение переключается, только если
новая цель весит заметно больше текущей. Иначе человек с двумя почти равными нуждами будет
метаться между столовой и туалетом, ничего не успевая.
Решение принимается **не каждый тик**, а по событиям: звонок, конец действия, приход в
назначенный узел, пересечение нуждой порога. Двадцать раз в секунду думать не о чем.
## Нужды
Декей включается. Голод — столовая, туалет — санузел, общение — перемена рядом с
одноклассниками. Сон **за кадром**: человек вне школы отдыхает, и утром приходит выспавшимся.
Ночной школы у нас нет, а изображать её ради одной полоски — лишняя работа.
Прогресс-бары нужд в карточке уже нарисованы. В этом срезе они впервые начнут шевелиться.
## Последствия — минимум
Осознанно мало:
- **Навык растёт на уроке** — от предмета, с поправкой на черты и на то, в каком человек
состоянии. Голодный учится хуже.
- **Опоздание и прогул видны.** В кабинете 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` — пока фильтрует клиент.
+5 -2
View File
@@ -10,7 +10,8 @@ Protocol — ни на кого из игровых проектов (
Content — ни на Protocol, ни на Simulation, ни на ASP.NET Content — ни на Protocol, ни на Simulation, ни на ASP.NET
People → Content (defs, карта, склонения; без Arch и хоста) People → Content (defs, карта, склонения; без Arch и хоста)
Schedule → Content (раскладка уроков; без Arch и хоста) Schedule → Content (раскладка уроков; без Arch и хоста)
Simulation → People, Content (ростер в World; без HTTP) Ai → Content, People, Schedule (маршруты и решения; без Arch и хоста)
Simulation → Ai, People, Content (ростер в World; без HTTP)
Server → Protocol, Simulation, People, Content Server → Protocol, Simulation, People, Content
Client — своя сторона Protocol (TS) + HTTP Client — своя сторона Protocol (TS) + HTTP
``` ```
@@ -24,7 +25,8 @@ Client — своя сторона Protocol (TS) + HTTP
| **HSchool.Content** | Типы def, JSONC-загрузчик, наследование, патчи, слияние локалей, граф карты, валидация связности | Arch, часы, HTTP, потоки, пути `mods/` с диска хоста | | **HSchool.Content** | Типы def, JSONC-загрузчик, наследование, патчи, слияние локалей, граф карты, валидация связности | Arch, часы, HTTP, потоки, пути `mods/` с диска хоста |
| **HSchool.People** | Генерация ростера: семьи, классы, тело/навыки/черты из каталога и сида | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва | | **HSchool.People** | Генерация ростера: семьи, классы, тело/навыки/черты из каталога и сида | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва |
| **HSchool.Schedule** | Раскладка учебного плана в таблицу уроков вокруг закреплённых правок | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва | | **HSchool.Schedule** | Раскладка учебного плана в таблицу уроков вокруг закреплённых правок | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва |
| **HSchool.Simulation** | `School`, `GameClock`, Arch `World`, тик, применение раскладки к миру | Kestrel, сокеты, `Directory.Enumerate`, сейв-файлы | | **HSchool.Ai** | Маршруты по графу карты, план дня человека, выбор цели и действия, продвижение плана во времени | Arch, ASP.NET, сокеты, `DateTime.Now`, файлы сейва |
| **HSchool.Simulation** | `School`, `GameClock`, Arch `World`, тик, применение раскладки к миру, адаптер к `Ai` | Kestrel, сокеты, `Directory.Enumerate`, сейв-файлы |
| **HSchool.Server** | Хост, супервизор, работник-поток, `mods/` и `saves/` из конфига, HTTP/WS, DTO запросов | Правила «комната должна быть связана», схема Chair | | **HSchool.Server** | Хост, супервизор, работник-поток, `mods/` и `saves/` из конфига, HTTP/WS, DTO запросов | Правила «комната должна быть связана», схема Chair |
| **HSchool.Protocol** | Кадры сокета, в том числе снимок карты и locale в Hello | Имена комнат, JSONC | | **HSchool.Protocol** | Кадры сокета, в том числе снимок карты и locale в Hello | Имена комнат, JSONC |
| **HSchool.Client** | DOM, `t()` для хрома, рисует снимок | Файлы модов, симуляция | | **HSchool.Client** | DOM, `t()` для хрома, рисует снимок | Файлы модов, симуляция |
@@ -52,6 +54,7 @@ Arch и фиксированный тик — про живую школу. Па
- `tests/HSchool.Content.Tests` — фикстуры JSONC, коллизии имён, дырявый граф, двор обязателен. - `tests/HSchool.Content.Tests` — фикстуры JSONC, коллизии имён, дырявый граф, двор обязателен.
- `tests/HSchool.People.Tests` — тот же сид даёт тот же ростер; места и должности заполнены. - `tests/HSchool.People.Tests` — тот же сид даёт тот же ростер; места и должности заполнены.
- `tests/HSchool.Schedule.Tests` — тот же штат и карта дают ту же таблицу; четыре запрета. - `tests/HSchool.Schedule.Tests` — тот же штат и карта дают ту же таблицу; четыре запрета.
- `tests/HSchool.Ai.Tests` — путь между комнатами и его цена; те же входы дают то же решение.
- `HSchool.Simulation.Tests` — часы, тик, «школа с таким каталогом живёт». - `HSchool.Simulation.Tests` — часы, тик, «школа с таким каталогом живёт».
- `HSchool.AppHost.Tests` — create с картой, рестарт, сейв на диске. - `HSchool.AppHost.Tests` — create с картой, рестарт, сейв на диске.
+79
View File
@@ -0,0 +1,79 @@
# Фаза 18. Присутствие и ходьба
## Зависимости
- [Фаза 16](16-timetable-in-school.md)
## Зачем
Человек перестаёт быть записью в расписании и получает **место**. Он приходит утром, идёт
коридором в кабинет и уходит после последнего урока. Экрана ещё нет — всё проверяется тестами
и логом.
## Задачи
- [ ] Новый проект `HSchool.Ai` рядом с `People` и `Schedule`: зависит на `Content`, `People` и
`Schedule`, без Arch и ASP.NET. Рядом — `tests/HSchool.Ai.Tests`
- [ ] `travelMinutes` на `RoomDef` и `TerritoryDef`; в `core` проставлены кабинет 0.5, коридор
1.5, лестница 1.5, вестибюль 1, двор 3
- [ ] Маршрутизация по `MapLayout.Links` — в библиотеке: матрица следующих шагов считается
**один раз** при загрузке школы, не на каждом шаге
- [ ] План дня человека — тоже в библиотеке: во сколько выходить, чтобы успеть, и когда уходить.
Часы, компоненты и мир туда не проникают
- [ ] Место человека — компонент в `World`: узел, остаток пути, минуты до выхода из узла.
Человек всегда в узле; рёбер как состояний нет
- [ ] Состояние «вне школы» — не удаление сущности: человек остаётся в списке и в карточке
- [ ] Приход и уход: у кого сегодня нет уроков, тот не приходит вовсе. Ученик и учитель — по
своим урокам, прочий штат — по дню школы
- [ ] Запас на дорогу свой у каждого: путь от двора плюс несколько минут из своего потока
случайности, с поправкой на черты
- [ ] Обязанность как место назначения: урок — кабинет из расписания, должность — комната из
`RoomDef.works`. Выбора действий пока нет
- [ ] Движение по тику: остаток минут в узле уменьшается, при нуле человек переходит в следующий
узел пути
- [ ] Стабильный порядок обхода людей — собственный список, а не порядок сущностей в Arch
- [ ] Адаптер в `HSchool.Simulation`: собирает значения из компонентов, зовёт библиотеку,
записывает ответ обратно. Горячий путь — движение — границу не пересекает
- [ ] Ориентир в `AGENTS.md`, `docs/architecture.md` и `docs/design/projects.md` знает про новый
проект
- [ ] Место, путь и назначение едут в файл часов; кого в файле нет, тот расставляется по
обязанности
- [ ] Потолок решений за тик — в `SimulationOptions`, как очередь, а не как отсечка
- [ ] Предикат «школа пуста»: в здании и во дворе нет никого
- [ ] Рабочее окно дня — от шести утра до последнего звонка по **каркасу дня**, а не по
фактическому расписанию
- [ ] «Ближайшее шесть утра рабочего дня впереди»: ночь ведёт в то же утро, вечер — в следующее,
выходные, праздники и каникулы пропускаются. Поиск ограничен разумным числом дней и
честно отвечает «нет такого»
- [ ] Прыжок часов туда одним шагом: разрешён только при пустой школе и вне рабочего окна,
отрабатывает первое сентября и обновление пула за весь пропуск, после себя пишет сейв
## Тесты, без которых фаза не закрыта
- [ ] Путь из кабинета 201 в санузел первого этажа идёт через лестницу и коридор, а не напрямую
- [ ] Сумма `travelMinutes` совпадает с временем, за которое человек реально доходит
- [ ] В воскресенье и в каникулы школа пуста: все вне школы
- [ ] Класс без уроков сегодня не приходит, а его учитель с уроками у другого класса — приходит
- [ ] К началу первого урока ученик находится в своём кабинете
- [ ] Физкультура после урока в главном корпусе: класс доходит до спортзала, и видно, что
перемены на это едва хватает
- [ ] Тот же сид и та же карта дают то же расположение людей на тот же момент
- [ ] Сохранение и загрузка посреди перемены не телепортируют людей
- [ ] Прыжок с субботы попадает в понедельник, 6:00; с трёх ночи вторника — в утро того же
вторника; с десяти вечера — в утро среды
- [ ] Прыжок в учебное время отклоняется. В семь утра рабочего дня, когда школа ещё пуста, —
тоже: это уже рабочее окно
- [ ] У школы без единого учителя рабочий день не пропускается
- [ ] Прыжок через летние каникулы проводит первое сентября: классы перешли, выпуск состоялся,
набор пришёл
- [ ] Промотанная неделя и прожитая неделя дают одно состояние школы
## Критерий готовности
- `dotnet test` проходит
- Маршруты и план дня проверены без мира: в `HSchool.Ai.Tests` нет Arch
- В `HSchool.Ai` нет упоминаний Arch, ASP.NET и `DateTime.Now`; в `HSchool.Simulation` — сокетов
## Стоп
Не трогать протокол и клиент. Не заводить нужды, действия и выбор — это фазы 20 и 21.
+55
View File
@@ -0,0 +1,55 @@
# Фаза 19. Присутствие на экране
## Зависимости
- [Фаза 18](18-presence-walking.md)
- [Фаза 17](17-timetable-screen.md)
## Зачем
Школа впервые видна живой: в дереве идут числа, на перемене коридор наполняется, к звонку
кабинеты сходятся обратно.
## Задачи
- [ ] Новое сообщение присутствия, ~2 раза в секунду, только по открытой школе: узлы со
счётчиками и люди с их узлом и состоянием
- [ ] Снимок карты становится **статическим**: люди и текущий урок уезжают в присутствие,
структура уходит один раз при открытии. Пересылка на смене слота убирается
- [ ] `ProtocolCodec.cs`, `protocol.ts` и `docs/protocol.md` правятся тем же коммитом, версия
бумпится до 7
- [ ] Справочник «id → имя» по HTTP: клиент забирает один раз при открытии школы и перечитывает,
увидев незнакомый id. Имена в кадр присутствия не кладутся
- [ ] Дерево: число людей рядом с узлом, живое. Пустой узел остаётся без числа
- [ ] Панель локации: кто здесь сейчас, списком с именами; урок в комнате берётся из присутствия,
а не из снимка
- [ ] Карточка человека: где он сейчас — узел, «в пути» или «вне школы»
- [ ] Присутствие приходит по всей школе, выбранный узел фильтрует клиент
- [ ] Кадр часов несёт разрешение на пропуск; клиент это разрешение **не вычисляет**
- [ ] Намерение «пропустить» — своё сообщение, как пауза и скорость. Сервер проверяет условия
заново: кадр из браузера не доверенный
- [ ] Кнопка рядом с паузой и скоростями: появляется только когда сервер разрешил, подписана
датой, к которой перенесёт
- [ ] Строки через `t(...)`, обе локали
## Тесты, без которых фаза не закрыта
- [ ] Байтовая раскладка присутствия сходится с обеих сторон
- [ ] Круговой прогон сообщения: сервер → клиент → те же значения
- [ ] Снимок карты больше не содержит людей и урока
- [ ] Кадр часов с разрешением на пропуск читается обеими сторонами одинаково
- [ ] Пропуск, присланный в учебное время, сервер отбрасывает, а не исполняет
## Критерий готовности
- Открыть школу в 8:25 и увидеть, как двор и вестибюль пустеют, а кабинеты наполняются
- На перемене коридор — самый людный узел дерева
- В каникулы дерево без чисел, а карточки говорят «вне школы»
- В пятницу после уроков дождаться, пока школа опустеет, нажать «пропустить» и оказаться в
понедельник в 6:00; вечером буднего дня та же кнопка ведёт в завтрашнее утро
- `dotnet test`, `npm test` и `npm run build` проходят
## Стоп
Не рисовать перемещение анимацией. Не заводить серверный `OpenLocation`. Не добавлять нужды и
действия.
+44
View File
@@ -0,0 +1,44 @@
# Фаза 20. Нужды и действия
## Зависимости
- [Фаза 19](19-presence-screen.md)
## Зачем
Нужды перестают быть четырьмя неподвижными полосками, а действия — пустым дефом. Появляется то,
из чего фаза 21 будет собирать решения.
## Задачи
- [ ] `ActionDef` получает поля: комната, вещь, длительность, восполняемая нужда и величина,
роли, вес досуга
- [ ] Действия в `core`: поесть в столовой, санузел, отдохнуть на перемене, пообщаться,
прогуляться по коридору и по двору
- [ ] Деф правил поведения — как `StaffingDef` у штата: пороги нужд, скорость обучения на уроке,
разброс запаса на дорогу, порог переключения решения
- [ ] Декей нужд включается в `core`. Сон восстанавливается вне школы, а не действием
- [ ] Действие занимает вещь на время: за один стол не садятся вдвоём
- [ ] Выполнение действия: начало, отсчёт минут, восполнение нужды по завершении. Правила — в
`HSchool.Ai`, компоненты и мир — в `HSchool.Simulation`
- [ ] Валидатор дефов ловит действие без комнаты, с неизвестной нуждой и с неизвестной вещью
- [ ] Карточка человека показывает, что он делает; полоски нужд начинают шевелиться
## Тесты, без которых фаза не закрыта
- [ ] Голод падает за игровой час ровно на `decayPerHour`
- [ ] Обед в столовой поднимает голод на заявленную величину и занимает заявленное время
- [ ] Столов меньше, чем желающих: лишние не начинают действие, а не садятся сквозь друг друга
- [ ] Ночь вне школы возвращает сон к максимуму
- [ ] Действие с неизвестной нуждой роняет загрузку каталога с внятной ошибкой
- [ ] Те же входы дают те же значения нужд на тот же момент
## Критерий готовности
- `dotnet test` и `npm test` проходят
- В карточке за игровой день видно, как нужды опускаются, а после столовой — поднимаются
## Стоп
Не подключать выбор действия — люди пока ходят только по обязанности. Не трогать оценки и
успеваемость.
+51
View File
@@ -0,0 +1,51 @@
# Фаза 21. Выбор действия
## Зависимости
- [Фаза 20](20-needs-actions.md)
## Зачем
Последний кусок среза: обязанность перестаёт быть приказом. Ученик с критической нуждой уходит с
урока и возвращается, и это видно в кабинете и в его карточке.
## Задачи
- [ ] Цели, веса и планирование — целиком в `HSchool.Ai`: на вход значения, на выход решение.
В `HSchool.Simulation` — только сбор входов и запись ответа
- [ ] Цели и их вес: обязанность — постоянный высокий, нужда — тем больше, чем ближе к нулю,
досуг — низкий и только при отсутствии обязанности
- [ ] Побеждает наибольший вес. Уход с урока получается из сравнения, а не из отдельного правила
- [ ] Планирование «дойти → сделать»: цель выбирает действие, планировщик достраивает дорогу до
подходящей комнаты
- [ ] Начатое доводится до конца: переключение только при заметном перевесе новой цели
- [ ] Решения по событиям — звонок, конец действия, приход в назначенный узел, пересечение
нуждой порога, — а не каждый тик
- [ ] Очередь решений с потолком за тик: отложенное принимается следующим тиком, а не пропадает
- [ ] Возвращение к обязанности после закрытия нужды, если урок ещё идёт
- [ ] Навык растёт на уроке: от предмета, с поправкой на черты и на состояние нужд
- [ ] Опоздание и отсутствие видны: в панели локации на уроке не весь класс, в карточке написано,
где человек
- [ ] Строки через `t(...)`, обе локали
## Тесты, без которых фаза не закрыта
- [ ] Ученик с нулевым «туалетом» уходит с урока, доходит до санузла и возвращается в кабинет
- [ ] Нужда чуть ниже порога урок не срывает
- [ ] Две почти равные нужды не заставляют метаться: действие доводится до конца
- [ ] На перемене выбирается досуг, на уроке — нет
- [ ] Навык после учебного дня вырос, у голодного — меньше
- [ ] Потолок решений за тик не теряет решения, а сдвигает их
- [ ] Тот же сид и те же действия игрока дают ту же школу через игровую неделю
- [ ] Разбор решения таблицей входов и выходов, без мира и без хоста
## Критерий готовности
- Прогнать школу на ×4 через учебный день и увидеть осмысленную картину: уроки, перемены с
коридором и столовой, редкие отлучки посреди урока
- `dotnet test`, `npm test` и `npm run build` проходят
## Стоп
Не добавлять оценки, настроение, конфликты и дружбу. Не давать игроку приказы. Не углублять
планировщик до полного GOAP.
+23
View File
@@ -68,3 +68,26 @@
| [15. Планировщик](15-timetable-planner.md) | ✅ | Раскладка часов по слотам, четыре запрета | | [15. Планировщик](15-timetable-planner.md) | ✅ | Раскладка часов по слотам, четыре запрета |
| [16. Расписание в школе](16-timetable-in-school.md) | ✅ | Сейв, снимок, «кто где сейчас» | | [16. Расписание в школе](16-timetable-in-school.md) | ✅ | Сейв, снимок, «кто где сейчас» |
| [17. Расписание на экране](17-timetable-screen.md) | ✅ | Скобки в дереве, сетка класса, расписание учителя | | [17. Расписание на экране](17-timetable-screen.md) | ✅ | Скобки в дереве, сетка класса, расписание учителя |
## Срез 5. Жизнь школы
Дизайн: [`../design/ai.md`](../design/ai.md).
Люди начинают ходить и решать. Присутствие перестаёт вычисляться из расписания и становится
состоянием: человек приходит утром, идёт коридором, может уйти с урока в туалет и вернуться.
Срез идёт **двумя этапами**, и между ними можно остановиться — школа останется целостной.
**Этап A — присутствие.** Люди ходят по обязанности; выбора у них ещё нет.
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [18. Присутствие и ходьба](18-presence-walking.md) | ⬜ | `HSchool.Ai`, место человека, маршруты, приход и уход |
| [19. Присутствие на экране](19-presence-screen.md) | ⬜ | Своё сообщение ~2 Гц, числа в дереве, статический снимок карты, пропуск пустого времени |
**Этап B — поведение.** Появляются нужды, действия и выбор между ними и обязанностью.
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [20. Нужды и действия](20-needs-actions.md) | ⬜ | `ActionDef` с полями, декей, восполнение |
| [21. Выбор действия](21-decisions.md) | ⬜ | Цели и веса, «дойти → сделать», уход с урока, рост навыка |