Enhance school day structure and decision-making for lunch breaks
ci / server (push) Failing after 3m43s
ci / client (push) Successful in 15s

- Updated `ai.md` to clarify the mechanics of hunger restoration and the importance of lunch breaks in the school schedule.
- Revised `schedule.md` to detail the new lunch break structure, allowing for separate sittings for different grade levels.
- Enhanced `Decision.cs` and `DecisionPlanner.cs` to incorporate logic for lunch breaks, ensuring that students only leave lessons during their designated lunch windows.
- Updated `DayFrameDef` and related classes to support multiple lunch breaks and validate their configurations.
- Adjusted tests to validate the new decision-making logic regarding lunch breaks and hunger management, ensuring robust functionality.
- Improved localization strings to reflect changes in the school day structure and lunch functionalities.
This commit is contained in:
Leonid Pershin
2026-08-19 23:17:16 +03:00
parent a441ed9763
commit 5eabc90d53
33 changed files with 831 additions and 404 deletions
+111 -5
View File
@@ -9,7 +9,7 @@
## Срез 1. Оболочка и карта
- **Фазы:** 04
- **Проверен на:** `b135a9c`, 2026-08-19
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `b135a9c`)
- **Пути:** `src/HSchool.Client`, `src/HSchool.Protocol`, `src/HSchool.Content`,
`src/HSchool.Server/Game`, `src/HSchool.Server/Api/ModEndpoints.cs`,
`src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Net`,
@@ -95,10 +95,29 @@
компилировалось. Выводам по срезу 1 это не мешает: его файлы правка не трогает, а дописанные
тесты прогнаны и на чистом `b135a9c`, и на дереве после починки сборки.
### Перепроверка на `5a00ad7`, 2026-08-19
Коммит добавил родные языки: `nativeLanguages` в каталоге, `nativeLanguage` в создании школы,
поле в сейве. Проверено по изменению:
- **Инвариант протокола цел.** Версия осталась `7` в обоих местах, `ProtocolCodec.cs` и
`protocol.ts` не тронуты — и правильно: изменение целиком в HTTP/JSON, который документ
объявляет расширяемым. `docs/protocol.md` пополнен тем же коммитом, включая новый код ошибки
`unknown-native-language`, и этот код проверяется тестом.
- **Формат сейва вырос на nullable-поле** без бампа `CurrentFormat`. Старый сейв без поля
загружается сознательно: `NativeLanguages.Pick(..., rollIfOmitted: false)` берёт первый язык
набора, а не бросает кубик, — иначе существующая школа сменила бы язык при первом же старте
сервера и следующий набор пришёл бы другим. Ветка была описана комментарием, но не проверялась.
- Дописанные в первый проход тесты (`unknown-mod` и побег из папки `mods`) проходят на новом коде.
Дописано: `SkillGrantTests.ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling`
закрывает всю таблицу `Pick`: старый сейв берёт первый язык, новая школа бросает кубик и бросок
стабилен по сиду, сохранённый выбор уважается, чужой id даёт `null`, из которого создание школы
делает `400`.
## Срез 2. Люди
- **Фазы:** 59
- **Проверен на:** `5cd5a6d`, 2026-08-19
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `5cd5a6d`)
- **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`,
`src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.cs`,
`src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Game/PersonCardReader.cs`,
@@ -176,10 +195,24 @@
идущего человека). По правилу журнала он больше не «проверен на `b135a9c`» — правка маленькая и
относится к срезу 5, но следующий проход это увидит.
### Перепроверка на `5a00ad7`, 2026-08-19
`5a00ad7` переписал половину генератора: `PersonSampler` (+266), `FamilyFactory` (+52),
`YearlyIntake` (+61), новый `NativeLanguages`, новые навыки в `core` (языки, социальные,
рабочие). Проверено по изменению:
- Решение записано заранее — `design/people.md` описывает родные языки и порядок выдачи навыков
ещё с `a441ed9`, код догнал документ. Дрейфа нет, это тот редкий случай, когда дизайн шёл первым.
- Восемь новых тестов в `SkillGrantTests` закрывают выдачу навыков по параллелям, отсутствие
рабочих навыков у учеников и набор без переброса уже выданного. Старые тесты детерминизма,
согласованности семьи и годового набора зелёные — генератор переписан, а обещания среза целы.
- Исправления среза 2 (карточка семьи, сортировка по фамилии, состав после набора и перезапуска)
проходят на новом коде.
## Срез 3. Штат
- **Фазы:** 1013
- **Проверен на:** `21d79cb`, 2026-08-19
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `21d79cb`)
- **Пути:** `src/HSchool.People/Staffing.cs`, `src/HSchool.People/ApplicantPool.cs`,
`src/HSchool.Content/SubjectDef` и `defs/staffing`, `src/HSchool.Server/Api/SchoolEndpoints.cs`
(staffing/hire/subjects), `src/HSchool.Server/mods/core/defs/{subjects,positions,rooms,staffing,traits}`,
@@ -249,10 +282,38 @@
- В `AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments` осталась ветка `if/else` вокруг
ожидаемого отказа. Теперь, когда отказ проверяется отдельным тестом, эта ветка просто не может
упасть — её стоит однажды упростить, но трогать работающий тест ради этого ревью не стало.
### Перепроверка на `5a00ad7`, 2026-08-19 — **вывод про бюджет исправлен**
`5a00ad7` поднял `poolSize` с 12 до 32. Это не косметика: предел фонда начал работать, и запись
выше про «предел почти не ограничивает игрока» больше не описывает игру. Перемерено на шести
сидах:
| что делаем | сколько людей | фонд из 100 000 | непокрыто |
| --- | --- | --- | --- |
| нанимать подряд, не назначая предметов | 20–21 из 32 | 95 00099 400, дальше отказ | всё |
| по учителю на предмет | 12 | 86 80094 700 | `PrimarySchool` |
| закрыть и его | 14 | 87 10095 100 | ничего |
То есть полностью укомплектованная школа теперь **влезает, но впритык** — запас 513%, и
`PrimarySchool` (1–4 классы, 80 часов в неделю против потолка в 36 на человека) требует трёх
учителей, о чём игрок узнаёт только из списка непокрытых.
Прежняя цифра «около 70 000 из 100 000» получена на вырожденной раскладке — когда каждому из
двенадцати назначены все двенадцать предметов. Так часы предмета делятся на всех и почти каждый
садится на пол одной ставки; это самая **дешёвая** конфигурация, а не «полностью
укомплектованная школа». Ошибка была в замере, а не в коде.
Ставки соискателей при этом не изменились (в среднем 57.9–59.3 против прежних 58.5–62.5), хотя
навыков у человека стало больше — цена часа считается от среднего по навыкам, и среднее устояло.
`Hire_IdlePool_FitsUnderTheCap` в том же коммите переписан так, чтобы принимать отказ по пределу
как штатный исход. Это правильно: с пулом в 32 человека нанять всех и остаться в пределах суммы
уже нельзя.
## Срез 4. Расписание
- **Фазы:** 1417
- **Проверен на:** `21d79cb`, 2026-08-19
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `21d79cb`)
- **Пути:** `src/HSchool.Schedule`, `src/HSchool.Content` (каркас дня, каникулы, `SubjectDef.room`),
`src/HSchool.Simulation/SchoolTimetables.cs`, `src/HSchool.Server/Api/TimetableEndpoints.cs`,
`src/HSchool.Server/Game/SchoolWorker.cs` (пин, перестроение),
@@ -319,10 +380,18 @@
прогона старых бинарников против новых данных, а не находка. Поэтому весь срез проверялся в
отдельном worktree на `21d79cb`: AppHost 63/63 (три прогона подряд), клиент 33/33 и сборка.
### Перепроверка на `5a00ad7`, 2026-08-19
Из путей среза коммит тронул только каталог навыков: пять строк ушли из `skills/academic.jsonc`,
появились `languages`, `social` и `work`. Предметы ссылаются на навыки долями, и обе защиты на
месте и зелёные — `Subject_UnknownSkill_FailsTheCatalog` и `EveryVanillaLabel_ExistsInBothLocales`.
Планировщик, каркас дня и API расписания не менялись; дописанный в прошлый проход тест на коды
отказа пина проходит.
## Срез 5. Жизнь школы
- **Фазы:** 1821
- **Проверен на:** `21d79cb`, 2026-08-19. Фаза 21 закрыта за час до проверки, `core` правился
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `21d79cb`). Фаза 21 закрыта за час до проверки, `core` правился
параллельно — проверка велась в отдельном worktree на коммите, не на рабочем дереве
- **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`,
`src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов),
@@ -402,3 +471,40 @@
случаях чинить надо тест, а не присутствие: занятость узла берётся из расписания, а явка — из
состояния, и совпадать они не обязаны — фаза 21 прямо обещает, что «опоздание и отсутствие
видны».
### Перепроверка на `5a00ad7`, 2026-08-19
`School.cs` изменился на десять строк — родной язык прокинут в `InstallPeople`. Присутствие,
ходьба, действия и решения не тронуты, `decayPerHour` и `needGain` тоже, поэтому измеренная выше
картина с голодом остаётся в силе: вопрос «чем закрывать голод» по-прежнему открыт.
## Что из журнала закрыто, 2026-08-19
Ревью нашло семь пунктов; шесть закрыты в этот же день, седьмой снят как несуществующий.
- **Панель «Общие события».** Решено отказаться. `design/near-term.md` и `design/people.md`
переписаны под вкладочную оболочку, фазы 1 и 8 помечены сноской, мёртвые ключи
`eventsTitle`/`eventsEmpty` убраны из `i18n`. Экран не менялся — он и был правдой.
- **Голод упирался в ноль.** Закрыто тремя правками данных и одной правкой поведения:
`Hunger` восстанавливается вне школы (дома едят), `decayPerHour` 0.1 → 0.2, стульев в столовой
8 → 112, и обед стал сменой по параллелям. Замер после: приходят с 0.97, к своей смене 0.43,
уходят с 0.27, к утру снова 1.0; за неделю голодающих нет ни одного (было 27 из 176 на нуле и
98 ниже порога).
- **Раздельные обеды.** Новая механика: `DayFrameDef.lunchBreaks` — какая параллель ест после
какого урока, `ActionDef.lunch` — действие-смена. В свою смену едят и не проголодавшиеся
(`DecisionPlanner.LunchWeight`), вне смены столовая не предлагается вовсе. В `core` младшие
(1–5) едят после третьего урока, старшие (6–11) после четвёртого; обе перемены стали длинными.
Замер: 11:10–11:30 в столовой только младшие, 12:15–12:30 только старшие, поели все 176.
- **`SchoolRegistry`.** Удалён вместе со своими тестами. `TryNormalizeName` переехал в
`SchoolNames`, `SchoolCreationError` — в свой файл; правила имени, диапазона дат и генератора
названий закреплены в `SchoolNamesTests`.
- **Нестабильный `OpeningASchoolDuringAMathLesson_...`.** Последнее ожидание требовало, чтобы
нанятый в середине дня учитель появился на территории, — а это не гарантировано: явка идёт из
плана дня, и замороженный момент уезжает тем дальше, чем дольше сам тест ходил по HTTP. Теперь
тест проверяет то, ради чего написан: что люди вообще едут в кадре присутствия.
- **Ветка `if/else` вокруг предела в `AssignAndUnassign_HonourTheCap_...`** убрана: отказ по
пределу проверяется отдельным тестом, а этот про назначение, снятие и перезапуск.
- **Предел фонда «почти не ограничивает игрока»** — снято при перепроверке на `5a00ad7`: это была
ошибка замера, а не код. Подробности в разделе среза 3.
Тесты после всех правок: 315 .NET и 33 клиентских, сборка клиента зелёная.