Files
h-school/docs/phases/reviewed.md
T
Leonid Pershin 5a00ad7746
ci / server (push) Failing after 3m44s
ci / client (push) Successful in 14s
Enhance school creation and staffing management with native language support
- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options.
- Enhanced the UI for school creation to allow selection of native languages, improving user experience.
- Revised API interfaces to accommodate new native language features, ensuring proper data handling.
- Improved localization strings to support new native language functionalities in both English and Russian.
- Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
2026-08-19 22:22:31 +03:00

40 KiB
Raw Blame History

Проверенные срезы

Журнал ревью. Фаза с в README.md — заявка автора; строка здесь — проверенный факт: каждое обещание фазы сверено с кодом и тестом, найденное дописано или исправлено.

Коммит в «Проверен на» делает журнал самопроверяющимся. Срез перестаёт считаться проверенным, как только git log <коммит>..HEAD -- <пути> перестаёт быть пустым — поэтому «Пути» обязательны.

Срез 1. Оболочка и карта

  • Фазы: 04
  • Проверен на: b135a9c, 2026-08-19
  • Пути: 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, tests/HSchool.Content.Tests, tests/HSchool.Protocol.Tests, tests/HSchool.AppHost.Tests/SchoolApiTests.cs, tests/HSchool.AppHost.Tests/GameSocketTests.cs
  • Итог: дописано 2 теста (оба прогнаны на чистом b135a9c), расхождений кода с дизайном не найдено кроме одного — панель «Общие события» из фазы 1 снята с экрана в фазе 8 и решение нигде не записано (оставлено открытым, см. ниже)

Что подтверждено:

  • Фаза 0. pixi.js нет ни в package.json, ни в lockfile, ни в импортах; npm run build проходит.
  • Фаза 1. Дерево карты, панель локации со всеми пятью секциями и пустыми состояниями, клик по дереву фильтрует на клиенте, localize() перекрашивает подписи. Панели «Общие события» на экране нет — единственное расхождение среза.
  • Фаза 2. SchoolWorkerLongRunning-поток, свой PeriodicTimer, свой World, аккумулятор с потолком в 5 шагов. Супервизор держит таблицу и не трогает World (инвариант 3). Сейв пишется на create, на shutdown и по SaveIntervalSeconds; настройки коалесцируются. Требуемый фазой тест «create → reload → та же школа с тем же временем» — это GameSocketTests.ReloadFromDisk_RestoresAPausedClock (точное равенство времени, паузы и скорости), плюс SchoolApiTests.ReloadFromDisk_RestoresCreatedSchools. Независимость школ — GameSocketTests.PausingOneSchool_DoesNotStopAnother.
  • Фаза 3. Все восемь тестов из списка фазы на месте в HSchool.Content.Tests (JSONC-фикстуры, last-wins по defName и по ключу локали, parent/abstract и цикл, патч add в actions, связность, пустая комната, одна комната без двора). Ванильная раскладка валидируется (VanillaCoreTests.CoreDefaultMap_PassesValidation). Пропавшая папка мода не стартует школу и не трогает файл: исключение летит до создания School, поэтому finally с Persist() недостижим, а супервизор только снимает школу с таблицы.
  • Фаза 4. GET /api/mods, GET /api/catalog?lang=, POST /api/schools с картой и модами, снимок карты при OpenSchool в языке Hello — всё покрыто тестами (OpeningASchool_SendsAMapSnapshot, LabelsTheSnapshotInTheHelloLocale, WithACustomMap_ReturnsThatLayout, ReloadFromDisk_RestoresACustomMap). Кодек, protocol.ts и docs/protocol.md описывают один и тот же MapSnapshot, байтовые смещения проверены с обеих сторон; версия 7 совпадает в ProtocolConstants и PROTOCOL_VERSION.
  • Инварианты. Content/People/Schedule/Ai не ссылаются на Arch и ASP.NET, Simulation — только на Arch; DateTime.Now/UtcNow в этих проектах нет.
  • «Things that will bite you». Ни одна из относящихся к срезу регрессий не вернулась: modal.ts резолвит промис явно, .dialog--screen держит height: fit-content, DefaultStartDate форсирует UTC-kind, аккумулятор работника на месте, MapSnapshotSize считается от сообщения, GameClient переносит оба WaitToReadAsync между итерациями, outbox роняет старейший кадр, меню патчит карточки через card.update.

Дописано:

  • SchoolApiTests.Catalog_WithAnUnknownMod_IsRejectedGET /api/catalog?mods= с неизвестной пачкой описан в docs/protocol.md как 400 unknown-mod, но не проверялся.
  • SchoolApiTests.ModId_ThatEscapesTheModsFolder_IsRejectedOnBothEndpoints — инвариант 6: id пачки приходит из браузера и подставляется в путь. ModContent.IsSafePackId его чистит на обоих входах (каталог и создание школы), тестом это закреплено не было.

Открыто:

  • Панель «Общие события» пропала. Фаза 1 требует три области, design/near-term.md держит их в «Что уже зафиксировано», а design/people.md описывает список людей как панель «рядом с картой и событиями». Коммит d8f8db6 (фаза 8) заменил панель вкладками Карта/Люди, не сказав об этом; ключи eventsTitle/eventsEmpty остались в i18n/strings.ts мёртвыми. Решать надо одно из двух: вернуть пустую панель (или третью вкладку) — или записать отказ от неё в дизайн и убрать ключи. Молча менять экран ревью не стало.

Замечено рядом (за границами среза):

  • SchoolRegistry после фазы 2 в бою не используется: сервер тикает школы работниками, а из класса вызывается только TryNormalizeName. SchoolRegistryTests.Tick_AdvancesOnlyRunningSchools проверяет путь, которым сервер больше не ходит.
  • PeopleApiTests.Card_IncludesFamilyAndLiveNeeds зависит от порядка тестов (фаза 8, срез 2). Проверено на чистом checkout b135a9c в отдельном worktree: в одиночку класс проходит (8/8), в полном прогоне класса падает на Assert.Equal(2, card.Family.Parents.Count) — ожидалось 2, пришёл 1. Причина: тест берёт People[0] и требует у него двух родителей, а состав семьи зависит от сида, сид — от id школы, id — от того, сколько школ создали тесты до него (и от saves/index.json на машине). На машине с накопленными сейвами тест зелёный, на чистом клоне — красный. Чинить надо тест: брать ученика, у которого два родителя, а не первого попавшегося. Неполные семьи — сознательное решение design/people.md, не баг генерации.
  • GameSocketTests.OpeningASchoolDuringAMathLesson_... не выдерживает полный прогон (срез 4). В одиночку проходит за 1 с, в полном прогоне падает с «No presence frame matched within 40 frames» через 22 с. Похоже на нехватку кадров под нагрузкой, а не на ошибку присутствия.
  • Полный dotnet test один раз упал: процесс HSchool.Simulation.Tests умер с 0xC0000005, успев отчитаться о 44 тестах из 63, и два ActivityTests (фаза 20) не прошли. Повторить не удалось. Скорее всего дерево правилось прямо во время прогона (см. ниже).
  • На момент проверки в рабочем дереве шла правка фазы 21 (Decision.cs, LessonLearning.cs, LessonLearningSystem.cs, PresenceSystem.cs, gameScreen.ts), и какое-то время решение не компилировалось. Выводам по срезу 1 это не мешает: его файлы правка не трогает, а дописанные тесты прогнаны и на чистом b135a9c, и на дереве после починки сборки.

Срез 2. Люди

  • Фазы: 59
  • Проверен на: 5cd5a6d, 2026-08-19
  • Пути: 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, src/HSchool.Client/src/ui/peoplePanel.ts, src/HSchool.Client/src/ui/personCard.ts, tests/HSchool.People.Tests, tests/HSchool.Content.Tests/PeopleDefTests.cs, tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs, tests/HSchool.AppHost.Tests/PeopleApiTests.cs
  • Итог: исправлен один тест, который падал на чистом клоне, укреплён второй, дописан один; расхождение с дизайном одно и то же, что в срезе 1 (раскладка оболочки менеджера)

Что подтверждено:

  • Фаза 5. Все шесть пунктов списка тестов на месте в PeopleDefTests (разбор каждого вида, наследование навыка, патч черты, неизвестная ссылка — ошибка каталога, склонение по модели и перекрытие явной таблицей), плюс ExtraPack_AddsANameSetAndTrait — критерий «мод со своим набором имён виден в каталоге». Наборы имён в GET /api/catalog проверены в SchoolApiTests.Catalog_LabelsDefsInTheRequestedLanguage.
  • Фаза 6. Все шесть тестов из списка фазы существуют и проходят. Критерий «в проекте нет сокетов, ECS и DateTime.Now» закреплён самими тестами — Assembly_DoesNotReferenceArchAspNetOrSockets и Sources_DoNotUseWallClock.
  • Фаза 7. Одиннадцать кабинетов (VanillaMap_HasElevenHomeroomsNotTheComputerLab), нужды тикают со школой, ростер публикуется снимком, PersistPeople зовётся только когда состав изменился — файл не переписывается по таймеру. Ростер, не сходящийся с картой, оставляет школу незапущенной (RosterFit.MatchesSchoolContentUnavailableException).
  • Фаза 8. Список читает опубликованный снимок (loop.FindPeople), карточка идёт через очередь команд с TaskCompletionSource — инвариант 3 цел. Все пять требуемых фазой тестов API на месте. Клиент: фильтры, сортируемые колонки, пейджер, семейные ссылки, всё через t(...); в строке списка только лёгкие поля.
  • Фаза 9. Выпуск старшей имеющейся параллели, сохранение вместимости, семьи через поколения и уход родителей выпускников покрыты в YearlyIntakeTests и PeopleInSchoolTests.
  • Дизайн. design/people.md сверен построчно: неполные семьи ≈8% (тест требует 5–10%), фамилия и отчество отцовские даже у одинокой матери, идентификатор ребёнка — счётчик, места раздаются вперемешку. Расхождений нет.

Исправлено:

  • PeopleApiTests.Card_IncludesFamilyAndLiveNeeds падал на чистом клоне. Тест брал первого ученика страницы и требовал у него ровно двух родителей. Состав семьи выводится из сида, а сид — это id школы (SchoolWorker.InstallPeople: seed = school.Id), то есть зависит от того, сколько школ создали тесты до него и что лежит в saves/index.json. Замер по сидам 1–30: у 28 первый пятиклассник с двумя родителями, у 2 — с одним, и полный прогон на свежем checkout b135a9c в отдельном worktree ловил как раз такой сид. Одинокие родители — сознательное решение дизайна, поэтому неправ был тест: теперь он ищет ученика, у которого есть мать, а количество родителей проверяет диапазоном. Проверено тем же способом, каким ловилось: полный прогон на чистом b135a9c трижды, 60/60.
  • PeopleApiTests.List_SortsBySurname мерил не тем. Тест требовал ordinal-порядка, а сервер ставит Ё на место Е (RosterBrowser.NameOrder, тест SurnameSort_FollowsTheAlphabetNotCodePoints). Совпадали они только потому, что в core нет фамилий на Ё — мод с такой фамилией уронил бы тест, а не код. Теперь тест складывает Ё в Е так же, как сервер.

Дописано:

  • PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition — критерии фаз 7 и 9 «перезапуск поднимает тех же людей» и «перезапуск после перехода поднимает новый состав» не проверялись сквозь сервер. Просто сравнить людей до и после reload недостаточно: генерация детерминирована от id школы, поэтому забывший файл сервер собрал бы тот же ростер и тест бы ничего не поймал. Тест поэтому ждёт 1 сентября (школа создаётся за 15 игровых минут до полуночи, это около трёх секунд реального времени), а после reload требует состав после набора и явно отличный от исходного.

Открыто:

  • Раскладка оболочки менеджера разошлась с дизайном — то же расхождение, что записано в срезе
    1. Фаза 8 обещает панель «Люди» нижним рядом во всю ширину, design/people.md фиксирует «панель в оболочке менеджера, нижний ряд; не отдельный экран», а на экране она вкладкой рядом с картой. Тот же коммит d8f8db6, что убрал панель событий. Решается одним решением на оба пункта: либо вернуть нижний ряд, либо переписать обе строки дизайна.

Замечено рядом (за границами среза):

  • design/people.md и задача фазы 5 говорят «скорость убывания нужд в core — ноль», а в defs/needs теперь decayPerHour от 0.05 до 0.15. Это сознательная работа фазы 20, не регрессия; строка дизайна просто устарела и относится к срезу 5.
  • Срез 1 успел сдвинуться: коммит 5cd5a6d тронул gameScreen.ts и strings.ts (подпись идущего человека). По правилу журнала он больше не «проверен на b135a9c» — правка маленькая и относится к срезу 5, но следующий проход это увидит.

Срез 3. Штат

  • Фазы: 1013
  • Проверен на: 21d79cb, 2026-08-19
  • Пути: 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}, src/HSchool.Client/src/ui/managementPanel.ts, src/HSchool.Client/src/net/api.ts, tests/HSchool.People.Tests/{StaffingTests,ApplicantPoolTests}.cs, tests/HSchool.AppHost.Tests/StaffingApiTests.cs
  • Итог: дописано 3 теста (один из них — прямо требуемый фазой 12 и отсутствовавший), дополнен docs/protocol.md; расхождений кода с дизайном не найдено

Что подтверждено:

  • Фаза 10. PETeacher в core нет, Classroom, ComputerLab и GymHall не открывают должностей — должности остались у библиотеки, приёмной, столовой, медкабинета и кабинета директора, ровно как обещано. Кабинет описан числом мест (seatThing/defaultSeats, пустой список слотов — проверено в Catalog_LabelsDefsInTheRequestedLanguage), редактор карты показывает у него одно поле «мест». Неизвестный навык в предмете роняет каталог (Subject_UnknownSkill_FailsTheCatalog), полнота локалей — EveryVanillaLabel_ExistsInBothLocales, стартовая дата 31 марта 2012 зашита в тестах API.
  • Фаза 11. Все шесть тестов из списка фазы на месте и проходят: детерминизм пула по сиду и неделе, частичное обновление с сохранением навыков и запроса, устойчивый размер за пятьдесят недель, «сильный просит больше», пустой штат при полной школе, соискатель вне ростера до найма.
  • Фаза 12. Фонд оплаты, деление часов между учителями предмета, пол одной ставки и покрытие предметов покрыты в StaffingTests. GET /staffing читает опубликованный снимок (loop.FindPeople), наём и назначение идут в воркер через GameCommand с TaskCompletionSource — инвариант 3 цел. Перезапуск с тем же штатом и назначениями — AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments.
  • Фаза 13. Строка денег, непокрытые предметы, список соискателей со ставкой и сильными навыками, кнопка найма с текстом ошибки, список штата с предметами и стоимостью, назначение и снятие с карточки — всё есть и всё через t(...).
  • Дизайн. design/staffing.md сверен построчно. Особо проверено: MaxWeeklyHours не запрещает назначение, а делает предмет непокрытым (в коде нет такой ошибки, и замер это подтверждает); цена часа считается от навыков и от черт.

Дописано:

  • StaffingApiTests.AssigningPastTheCap_IsRejectedWithTheNumbers — фаза 12 требует теста API на «отклонение по пределу», а его не было: единственное место, где он мог сработать, было написано через if (over.StatusCode == Conflict) … else … и проходило при любом исходе. Пределом занялся замер: при выделенных 100 000 полностью загруженная школа стоит всего 68–72 тысячи, то есть «нанять всех» предела не достигает. Но пик фонда приходится на середину раздачи предметов — пока часы предмета несут один-два учителя, а не двенадцать, — и там отказ случается: по сидам 1–30 отказов от 4 до 24 на прогон, ни одного сида без отказа. Тест раздаёт предметы штату, ловит первый 409, сверяет payroll-exceeded, allocated, attempted > allocated, remaining и то, что отклонённое назначение действительно не легло.
  • ApplicantPoolTests.ConfidentTraits_AskForMoreAndQuietOnesForLess — дизайн фиксирует «цена часа: от навыков и от черт (самоуверенный просит больше)», код это делает через TraitDef.WageAskcore Leader +10, HotTempered +6, Quiet 8), а тестом закреплена была только половина про навыки.
  • Проверка в AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments, что после перезапуска возвращается тот же пул с теми же ставками — критерий готовности фазы 11, который никто не проверял. Тест кусается: пул на диске уже без нанятого, а сгенерированный заново вернул бы его обратно.

Исправлено:

  • docs/protocol.md не упоминал ?lang=ru|en у POST staff/hire, POST …/subjects и DELETE …/subjects/{subject}, хотя все три его принимают и клиент его шлёт. Поведение не трогалось — дописана документация.

Замечено рядом (за границами среза):

  • Предел фонда при ванильных данных почти не ограничивает игрока: нанять всех двенадцать и раздать им все предметы стоит около 70 000 из 100 000. Отказ достижим только по дороге, на пике неравномерной нагрузки. Это игровой баланс, а не ошибка (см. коммит 207e7ec, которым предел поднимали намеренно), но если предел задумывался как настоящее ограничение — числа сейчас говорят обратное.
  • В AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments осталась ветка if/else вокруг ожидаемого отказа. Теперь, когда отказ проверяется отдельным тестом, эта ветка просто не может упасть — её стоит однажды упростить, но трогать работающий тест ради этого ревью не стало.

Срез 4. Расписание

  • Фазы: 1417
  • Проверен на: 21d79cb, 2026-08-19
  • Пути: 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 (пин, перестроение), src/HSchool.Client/src/ui/timetableGrid.ts, src/HSchool.Client/src/ui/gameScreen.ts, tests/HSchool.Schedule.Tests, tests/HSchool.Content.Tests/CalendarTests.cs, tests/HSchool.AppHost.Tests/TimetableApiTests.cs
  • Итог: дописан один тест на пять непокрытых кодов отказа, поправлены две устаревшие строки дизайна; расхождений кода с принятыми решениями нет

Что подтверждено:

  • Фаза 14. Все пункты критерия закрыты в CalendarTests: суббота, каникулы и ночь дают «вне учебного дня», 10:20 буднего дня — третий урок, шестидневка делает субботу рабочей, зимние каникулы переносятся через 1 января. Требование помещения у предмета есть (физкультура — спортзал), неизвестная комната роняет каталог.
  • Фаза 15. Все шесть тестов из списка фазы на месте. Четыре запрета проверяются одним утверждением в TeacherWithTwoSubjects_IsNeverInTwoRoomsAtOnce (уникальность по учителю, классу и кабинету в слоте) плюс ClassOfSixteen_DoesNotEnterALabOfTwelve. Отсутствие сокетов, ECS и часов реального времени — самопроверкой сборки, как в HSchool.People.
  • Фаза 16. Расписание пересобирается только когда изменился состав: TimetableDirty ставится в InstallPeople/Replace и в годовом наборе, а воркер перестраивает таблицу под if (result.PeopleChanged) — по тику этого не происходит. GET /timetable читает опубликованный снимок, пин и снятие идут через GameCommand с TaskCompletionSource. Перезапуск с закреплениями — Reload_RestoresLockedLessons.
  • Фаза 17. Скобки в дереве (Математика · 5Б) собираются из присутствия, сетка «день × урок» с переносом, сменой кабинета и отметкой «закреплено» есть, отказ планировщика показывается текстом (timetableErrorPin, timetableErrorNoTeacher, timetableErrorLesson), личное расписание — в карточке. Всё через t(...). Клиентские тесты и сборка на 21d79cb: 33/33 и зелёная сборка.

Дописано:

  • TimetableApiTests.PinAndUnpinRefusals_CarryTheDocumentedCodes — из шести кодов отказа пина в docs/protocol.md тестом был закрыт только pin-rejected (хвост PinThenHireAnother_KeepsTheLockedSlot, попытка провести урок в спортзале). Остальные пять — no-teacher, unknown-class, unknown-subject, unknown-room, unknown-lesson на повторном снятии, плюс invalid-query на неполном запросе — не проверялись, хотя клиент по ним переключает текст ошибки.

Исправлено:

  • design/schedule.md держал в «Зафиксировано» две строки, которые срез 5 сознательно отменил: «Панель локации заполняется из расписания» и «Люди находятся по расписанию; ходьбы нет». Отмена записана в design/ai.md («присутствие — состояние человека, а не функция от расписания»), а старый документ об этом не знал. Помечены как отменённые со ссылкой; код не трогался — он давно живёт по новому решению.

Замечено рядом (за границами среза):

  • Задача фазы 16 «снимок карты отдаёт по узлу, что там сейчас идёт и у кого» в протоколе v7 выглядит иначе: снимок карты стал статическим, а занятость уехала в кадр Presence. Это сделано фазой 19 намеренно и описано в docs/protocol.md; строка фазы 16 просто историческая.
  • GameSocketTests.OpeningASchoolDuringAMathLesson_..., который в срезе 1 падал дважды под нагрузкой, за этот проход не воспроизвёлся ни разу: шесть полных прогонов подряд на чистом worktree и два одиночных под искусственной нагрузкой из шести жгущих процессов — зелено. Оставляю как «наблюдалось, не воспроизводится»; если вернётся, смотреть на выдачу кадров присутствия под нехваткой процессора, а не на логику.

Про окружение:

  • Во время проверки параллельная сессия правила core и клиент (размер пула соискателей 12 → 32, новые навыки, диалог соискателей). Два прогона на рабочем дереве успели показать «падения» Hire_IdlePool_FitsUnderTheCap и GetStaffing_EmptySchool_HasPoolAndNoPayroll — это артефакт прогона старых бинарников против новых данных, а не находка. Поэтому весь срез проверялся в отдельном worktree на 21d79cb: AppHost 63/63 (три прогона подряд), клиент 33/33 и сборка.

Срез 5. Жизнь школы

  • Фазы: 1821
  • Проверен на: 21d79cb, 2026-08-19. Фаза 21 закрыта за час до проверки, core правился параллельно — проверка велась в отдельном worktree на коммите, не на рабочем дереве
  • Пути: src/HSchool.Ai, src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs, src/HSchool.Protocol (кадр Presence, пропуск в кадре часов), src/HSchool.Server/Game/PresenceFrame.cs, src/HSchool.Client/src/ui/gameScreen.ts, src/HSchool.Server/mods/core/defs/{actions,behavior,needs}, tests/HSchool.Ai.Tests, tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests}.cs
  • Итог: все 32 теста из списков четырёх фаз на месте и проходят; тестов не дописано, зато измерен прожитый учебный день — и он показал две вещи, которых списки не ловят

Что подтверждено:

  • Фаза 18. Все 13 требуемых тестов есть: маршрут 201 → санузел через лестницу, совпадение суммы travelMinutes с реально пройденным временем, пустая школа в воскресенье и в каникулы, класс без уроков не приходит, ученик в кабинете к звонку, спортзал впритык к перемене, детерминизм по сиду, сохранение посреди перемены, четыре сценария пропуска пустого времени, школа без учителей, лето с первым сентября, промотанная неделя против прожитой.
  • Фаза 19. Все пять тестов: байтовая раскладка присутствия с обеих сторон, круговой прогон, отсутствие людей и урока в снимке карты, разрешение на пропуск в кадре часов, отброшенный пропуск в учебное время.
  • Фаза 20. Все шесть: декей голода за час, обед на заявленные минуты и величину, нехватка стульев, ночь вне школы, действие с неизвестной нуждой роняет каталог, детерминизм нужд.
  • Фаза 21. Все восемь, включая сквозной ZeroToilet_LeavesClass_ReachesRestroom_AndReturns, DecisionCap_DefersTheOverflow и SameSeedAndActions_MatchAfterAWeek, плюс табличный разбор решений в DecisionPlannerTests без мира.
  • Инварианты. HSchool.Ai не знает Arch, ASP.NET и часов реального времени, HSchool.Simulation не знает сокетов — обе проверки в самих тестах. Протокол v7 сходится в трёх местах.

Измерено (временный тест, удалён после):

Полностью укомплектованная ванильная школа (12 учителей, 225 уроков, 176 учеников), прожитая тиками с вторника по пятницу:

день 1 день 2 день 3 день 4
побывали в столовой 0 144 133 141
завершённых обедов 0 75 132 111
минимальный голод 0.32 0.00 0.00 0.00

Картина дня в остальном осмысленная: в 8:25 двор, вестибюль и лестницы полны, к 8:45 все 144 пришедших в кабинетах и спортзале, на перемене коридор — самый людный узел школы (пик 112), после уроков остаются только те, у кого урок ещё идёт.

  • Столовая пустует весь первый день. Голод стартует с 1.0 и падает на 0.1 в игровой час только на территории школы, так что порог 0.35 достигается лишь к концу первого дня. Критерий готовности фазы 21 обещает «перемены с коридором и столовой» — со второго дня так и есть, но прогон одного дня, как в критерии написано, столовой не покажет. Это не поломка, это то, чего стоит ожидать при проверке руками.
  • Со второго дня голод упирается в ноль и оттуда не выходит. К концу четырёх учебных дней 27 учеников из 176 держат голод ≤0.05, 98 из 176 — ниже порога срочности, средний по школе ровно 0.35. Причина арифметическая: вне школы голод не восстанавливается (в отличие от сна), дома никто не ест, а единственный источник еды — восемь стульев в столовой на 176 человек при обеде в 15 минут. Обедов в день выходит 75–132, то есть до половины школы ежедневно остаётся без еды, и дефицит переносится на завтра. Поскольку рост навыка на уроке зависит от состояния нужд, больше половины школы учится со штрафом постоянно. Это баланс, а не логика: числа лежат в core (decayPerHour, needGain, число стульев на карте) и правятся данными. Молча их менять ревью не стало — см. «Открыто».

Открыто:

  • Чем закрывать голод. Вариантов три, и это решение автора, а не ревью: восстанавливать голод вне школы, как сон (restoredOffCampus на Hunger); поставить в ванильную столовую больше стульев; или снизить decayPerHour голода. Первое честнее всего описывает «поел дома», третье дешевле всего, второе меняет карту. Числа выше дают точку отсчёта для любого из них.

Замечено рядом (за границами среза):

  • GameSocketTests.OpeningASchoolDuringAMathLesson_... подтверждённо нестабилен. За сессию он упал четырежды — и все четыре раза в полном dotnet test по решению, когда параллельно шла сборка; в одиночку он проходит всегда, в том числе шесть прогонов подряд и два под нагрузкой из шести жгущих процессов. Симптом всегда один: последнее ожидание после снятия паузы — «нанятый учитель появился среди людей в кадре присутствия» — не выполняется за 40 кадров (~20 секунд). Механизм не пойман: инструментированный прогон, печатающий замороженное время и уроки этого учителя, три раза подряд прошёл зелёным. Две правдоподобные версии, обе про то, что тест зависит от игрового времени, набежавшего за время его собственных HTTP-вызовов (5 игровых минут на реальную секунду): либо замороженный момент уезжает за последний урок учителя и он по плану дня уже не приходит, либо за 20 секунд ожидания школьный день успевает кончиться. В обоих случаях чинить надо тест, а не присутствие: занятость узла берётся из расписания, а явка — из состояния, и совпадать они не обязаны — фаза 21 прямо обещает, что «опоздание и отсутствие видны».