diff --git a/docs/phases/07-inventory/reviewed.md b/docs/phases/07-inventory/reviewed.md index dbf3b38..acd697e 100644 --- a/docs/phases/07-inventory/reviewed.md +++ b/docs/phases/07-inventory/reviewed.md @@ -58,48 +58,67 @@ берёт первую страну каталога — это create, не сейв. - Этап B–C в этом проходе не проверялся. -## Срез 7. Этап B–C — климат, жизнь одежды и правила +## Срез 7. Этап B–C — климат, жизнь одежды и правила (перепроверка) - **Фазы:** 32–37 -- **Проверен на:** `5dcdee6`, 2026-08-20 -- **Пути:** `src/HSchool.Simulation/{WeatherSampler,WarmthDecay,PlaceClimate,ApparelDresser,ApparelWear,MorningDress,PersonLogBrowser}.cs`, +- **Проверен на:** `505bcc6`, 2026-08-20 — **перепроверка** после `5dcdee6` +- **Пути:** `src/HSchool.Simulation/{WeatherSampler,WarmthDecay,PlaceClimate,ApparelDresser,ApparelWear,MorningDress,PersonLogBrowser,School}.cs`, `src/HSchool.People/LockerAssigner.cs`, `src/HSchool.Content/{Appropriateness,ClimatePreset,DressRules}.cs`, `src/HSchool.Ai/ApparelGoals.cs`, `src/HSchool.Protocol`, `src/HSchool.Server/Api/{SchoolEndpoints,DressRulesModels}.cs`, - `src/HSchool.Server/Game/{PersonCardReader,SchoolWorker}.cs`, `src/HSchool.Client/src/{ui/dressRulesPanel.ts,ui/managementPanel.ts,format/weather.ts,net/api.ts}`, + `src/HSchool.Server/Game/{PersonCardReader,SchoolWorker.Tick}.cs`, `src/HSchool.Client/src/{ui/dressRulesPanel.ts,ui/managementPanel.ts,format/weather.ts,net/api.ts}`, `docs/protocol.md`, `tests/HSchool.Simulation.Tests`, `tests/HSchool.People.Tests/LockerAssignerTests.cs`, `tests/HSchool.Content.Tests/{ClimatePresetTests,AppropriatenessTests,ChangingRoomMapTests}.cs`, `tests/HSchool.AppHost.Tests/{PersonLogApiTests,DressRulesApiTests}.cs`, `tests/HSchool.Protocol.Tests`, `src/HSchool.Client/src/ui/{personCard,dressRulesPanel,managementPanel}.test.ts` -- **Итог:** фазы 32–37 подтверждены; дописано 5 тестов симуляции; исправлены 2 бага переодевания и 2 - регрессии тестов (не протокол) +- **Итог:** обещания 32–37 после дрейфа (сессия, разговоры, сплит воркера и карточки, протокол v9) + в целом держатся; дописан тест «куртка не телепортируется»; три PE-теста прошлого прохода + красные на текущем main — открыто Что подтверждено: -- **32–36:** погода детерминирована; skip выставляет утро; кадр часов v8 (27 байт) в трёх местах; - `Warmth` и техник; шкафчики; износ и утренняя замена; уместность и PE; вкладки карточки и HTTP-лог. -- **37:** вкладка «Правила» в «Управлении»; четыре комбобокса; GET/POST `dress-rules` с pending; - `unknown-form`/`unknown-color` → `400`; правило с завтра; `ColorAllowed` для `whiteTopBlackBottom`; - `dressRulesPanel.test.ts` — combobox без API до Apply, ошибка текстом; `ManagementPanel` не ломает штат - и сетку. +- **32:** тот же пресет+сид+время → та же улица; январь холоднее июля; ниже нуля снег; + skip ставит утро понедельника, не субботу; naked на морозе теряет `Warmth`, дома возвращает; + кадр часов 27 байт (tenths + precipitation) в `ProtocolCodec` / `protocol.ts` / `docs/protocol.md`; + клиент рисует кадр, не считает месяц (`weather.test.ts`); Hello со старой версией отваливается. + Версия протокола сейчас 9 (круги разговоров), раскладка часов не менялась. +- **33:** кабинет без техника теплее улицы и холоднее, чем с техником; в июле техник держит + ниже улицы; две раздевалки, граф связен; 10 шкафчиков — старшие девочки; мальчик не в + девичьей; выпускник слот не держит; `ChangingSpot` (4) отдельно от шкафчиков. +- **34:** час на теле снижает прочность на `ApparelWearPerHour`; сумка и шкафчик нет; + skip через ночь = прожитая ночь; подпись порогов с сервера, клиент не считает. +- **35:** «без яркого» утром; короткая форма и возраст; PE: доходят до своей раздевалки + (`PeBreak_PupilReachesOwnChangingRoom`); мальчик не в женской; без шкафчика снятое в сумку; + мороз — `Outer`, в июле шубы нет; pending правило с завтра. Куртка при смене погоды не + телепортируется (урок держит, длинная перемена — ходьба и смена). +- **36:** лог не в карточке; страницы не пересекаются; 404 unknown-person; после 6:00 вчера + пусто; вкладка «Одежда» без таблицы лога; «Сейчас» вне школы — «дома». +- **37:** GET дефолт обычная/без яркого; POST unknown-form/color → 400; вкладка не ломает штат + и сетку; четыре комбобокса одежды на месте (речь среза 9 рядом, не вместо). -Дописано (32–36, прошлый проход): +Дописано: -- `AppropriatenessSimulationTests.MorningDress_StagesPeKitForTodaysLesson` -- `AppropriatenessSimulationTests.PeBreak_CompletesChangeBeforeLesson` -- `AppropriatenessSimulationTests.PeLesson_WearsPeThenReturnsToEveryday` -- `AppropriatenessSimulationTests.AfterPeBreak_ModeExpectsEveryday` -- `PresenceTests.PhysicalEducationBeforeLesson_GoesToChangingRoomOnTheBreak` +- `AppropriatenessSimulationTests.WeatherDrop_WalksToChangingRoom_DoesNotTeleportJacket` +- `personCard.test.ts` — `hasLocker: true` показывает «есть шкафчик» Исправлено: -- `PresenceSystem`: ходьба по duty не обрывает активный `ChangeClothes`. -- `ApparelPresence.ModeAt`: после перемены following PE — режим everyday. -- `managementPanel.test.ts`: панель штата скрывается через `hidden`, не удаляется из DOM. -- `PortraitApiTests.GetSwarmUiSettings_ReturnsPresets`: URL `/api/settings/swarmui` (CS1503). -- `dressRulesPanel.ts`: неиспользуемый импорт `clear` — TS6133 на `npm run build`. +- (кода симуляции PE не меняли: попытка починить звонок/очередь в раздевалке не закрыла + красные PE-тесты; правки откатили) + +Открыто: + +- `PeBreak_CompletesChangeBeforeLesson`, `PeLesson_WearsPeThenReturnsToEveryday`, + `AfterPeBreak_ModeExpectsEveryday` красные уже на `505bcc6` без правок этого прохода. + В 9:24 ученик в своей раздевалке, к 9:30 — в зале в футболке: 4 `ChangingSpot`, звонок + (duty 10 > одежда 6) уводит до `ChangeClothes`; праздные в комнате не перерешаются, когда + место освобождается. Это дрейф после разговоров/конфликта, не чинили молча в срезе 9. +- Короткая уместность на вкладке «Одежда» по-прежнему прочерк: сервер не шлёт подпись + (`«к уроку»` / `«холодно»` / `«не по форме»`). Молча расширять HTTP ревью не стало. +- Короткой перемены (10 мин) с дальнего кабинета не хватает дойти и переодеться; тест погоды + идёт через длинную после 3 урока. Замечено рядом: -- `PhysicalEducationBeforeLesson_…` не гоняет полный цикл «зал с формой» — это в - `AppropriatenessSimulationTests.PeLesson_…`. -- Портреты/SwarmUI — вне среза 7, но `PortraitApiTests` чинился, чтобы AppHost-сборка не падала. +- `dressRulesPanel` рисует ещё комбобоксы речи (срез 9) — четыре комбобокса одежды на месте. +- Кадр часов остался 27 байт при PROTOCOL_VERSION 9. +- AppHost `PersonLogApiTests` / `DressRulesApiTests` в этом проходе не гонялись (хост). diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 60661b2..30584b1 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -27,7 +27,7 @@ | Срез 5. Жизнь школы | `1118c59` | [05-ai](05-ai/reviewed.md) | | Срез 6. Фундамент | `1c63357` | [06-foundation](06-foundation/reviewed.md) | | Срез 7. Этап A — данные и гардероб | `5468852` | [07-inventory](07-inventory/reviewed.md) | -| Срез 7. Этап B–C — климат, жизнь одежды и правила | `5dcdee6` | [07-inventory](07-inventory/reviewed.md) | +| Срез 7. Этап B–C — климат, жизнь одежды и правила | `505bcc6` | [07-inventory](07-inventory/reviewed.md) | | Срез 8. Сессия и темп | `7100065` | [08-session](08-session/reviewed.md) | | Срез 9. Этапы A–B — мнения и разговоры | `b38d0bb` | [09-social](09-social/reviewed.md) | | Срез 9. Этапы C–D — конфликт, присутствие, речь и romance | `19554d4` | [09-social](09-social/reviewed.md) | diff --git a/src/HSchool.Client/src/ui/personCard.test.ts b/src/HSchool.Client/src/ui/personCard.test.ts index ecc2dff..b942c4d 100644 --- a/src/HSchool.Client/src/ui/personCard.test.ts +++ b/src/HSchool.Client/src/ui/personCard.test.ts @@ -142,6 +142,11 @@ describe('renderPersonCard', () => { expect(root.textContent).toContain(t('peopleHomeCount', { n: 2 })); expect(root.textContent).toContain('Учебник · Математика'); + root.replaceChildren(); + renderPersonCard(root, card({ hasLocker: true }), () => {}, options({ tab: 'carry' })); + expect(root.textContent).toContain(t('peopleLockerYes')); + expect(root.textContent).not.toContain(t('peopleLockerNone')); + tabButton(root, 'now').click(); expect(root.querySelector('[data-card-tab="now"]')?.hasAttribute('hidden')).toBe(false); }); diff --git a/src/HSchool.Simulation/School.cs b/src/HSchool.Simulation/School.cs index 4bb2e41..98c1d4d 100644 --- a/src/HSchool.Simulation/School.cs +++ b/src/HSchool.Simulation/School.cs @@ -422,7 +422,18 @@ public sealed class School : IDisposable public void SyncWeather(bool force) { ObjectDisposedException.ThrowIf(_disposed, this); - var next = EvaluateWeather(); + CommitWeather(EvaluateWeather(), force); + } + + /// + /// Test hook: the vanilla diurnal never crosses outerBelowC during a school day, so a + /// mid-day frost has to be injected to prove jackets walk to the locker room instead of + /// appearing on the body. + /// + internal void ForceWeather(OutdoorWeather weather) => CommitWeather(weather, force: true); + + private void CommitWeather(OutdoorWeather next, bool force) + { if (force || next.Tenths != Weather.Tenths || next.Precipitation != Weather.Precipitation) { var before = Weather; diff --git a/tests/HSchool.Simulation.Tests/AppropriatenessSimulationTests.cs b/tests/HSchool.Simulation.Tests/AppropriatenessSimulationTests.cs index 56662b8..73e9e62 100644 --- a/tests/HSchool.Simulation.Tests/AppropriatenessSimulationTests.cs +++ b/tests/HSchool.Simulation.Tests/AppropriatenessSimulationTests.cs @@ -249,6 +249,70 @@ public class AppropriatenessSimulationTests Assert.Equal(0, BrightWornCount(school)); } + [Fact] + public void WeatherDrop_WalksToChangingRoom_DoesNotTeleportJacket() + { + var (school, pupilId) = StaffedClassWithLocker(); + using (school) + { + MorningDress.Apply(school); + AdvanceTo(school, new DateTime(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc)); + + var here = school.CapturePresence() + .Where(row => row.NodeId is not null) + .Select(row => row.PersonId) + .ToHashSet(StringComparer.Ordinal); + Assert.Contains(pupilId, here); + var pupil = school.Roster!.People.Single(person => person.Id == pupilId); + var items = pupil.Items + .Select(item => + item.Location == ItemLocations.Worn + && school.Catalog!.Things[item.Def].Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal) + ? item with { Location = ItemLocations.Locker } + : item) + .ToList(); + items.Add(new InventoryItem("Jacket", "Blue", 1f, ItemLocations.Locker)); + ReplaceItems(pupil, items); + + school.ForceWeather(new OutdoorWeather(-8f, Precipitation.Snow)); + pupil = school.Roster.People.Single(person => person.Id == pupil.Id); + Assert.DoesNotContain( + pupil.Items, + item => item.Location == ItemLocations.Worn + && school.Catalog!.Things[item.Def].Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal)); + + AdvanceTo(school, new DateTime(2012, 4, 3, 8, 32, 0, DateTimeKind.Utc)); + pupil = school.Roster.People.Single(person => person.Id == pupil.Id); + Assert.DoesNotContain( + pupil.Items, + item => item.Location == ItemLocations.Worn + && school.Catalog!.Things[item.Def].Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal)); + var duringLesson = school.CapturePresence().Single(item => item.PersonId == pupil.Id); + Assert.NotEqual( + pupil.Female ? "girls-changing-room" : "boys-changing-room", + duringLesson.NodeId); + + AdvanceTo(school, new DateTime(2012, 4, 3, 11, 08, 0, DateTimeKind.Utc)); + pupil = school.Roster.People.Single(person => person.Id == pupil.Id); + Assert.DoesNotContain( + pupil.Items, + item => item.Location == ItemLocations.Worn + && school.Catalog!.Things[item.Def].Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal)); + var row = school.CapturePresence().Single(item => item.PersonId == pupil.Id); + var room = pupil.Female ? "girls-changing-room" : "boys-changing-room"; + Assert.True( + row.NodeId == room || row.Path.Contains(room, StringComparer.Ordinal), + $"expected a walk toward {room}, sat at {row.NodeId} path [{string.Join(',', row.Path)}]"); + + AdvanceTo(school, new DateTime(2012, 4, 3, 11, 20, 0, DateTimeKind.Utc)); + pupil = school.Roster.People.Single(person => person.Id == pupil.Id); + Assert.Contains( + pupil.Items, + item => item.Location == ItemLocations.Worn + && school.Catalog!.Things[item.Def].Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal)); + } + } + [Fact] public void WhiteTopBlackBottom_AfterMorning_WhiteShirtsOnStudents() { @@ -317,6 +381,29 @@ public class AppropriatenessSimulationTests } } + private static (School School, string PupilId) StaffedClassWithLocker() + { + var (catalog, map) = Vanilla(); + var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning); + roster = LockerAssigner.Apply(catalog, map, roster); + roster = DressGenerator.EnsureRoster(catalog, roster, schoolSeed: 1, TuesdayMorning); + var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning); + var pupil = roster.People.First(person => person.IsStudent && person.LockerRoomId is not null); + var schoolClass = roster.Classes.First(row => row.PupilIds.Contains(pupil.Id)); + var school = School.Create(1, "Погода одежда", TuesdayMorning, catalog, map); + school.InstallPeople(roster, seed: 1, "Russia", pool, climatePresetId: "TemperateContinental"); + school.SetTimetable(new Timetable( + [ + new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1), + new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 2), + new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 3), + new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 4), + ], + [])); + school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); + return (school, pupil.Id); + } + private static (School School, string PupilId) StaffedPeClass(bool forceBoy = false) { var (catalog, map) = Vanilla();