From aa20534b77bb12ddee04082d4e4a3f5b4ca1e219 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 17:11:06 +0300 Subject: [PATCH 1/2] Add tests that lesson-consequence promises were missing. Teacher action must not cancel gain, a missing warmth need matches full warmth, snow on the street must reach DayPlans, and a lazy pupil still appears closer to the bell. --- docs/phases/off-queue/53-weather-commute.md | 3 +- tests/HSchool.Ai.Tests/WalkingTests.cs | 24 +++++++ .../LessonTeacherPresentTests.cs | 70 +++++++++++++++++++ .../HSchool.Simulation.Tests/WeatherTests.cs | 39 +++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/docs/phases/off-queue/53-weather-commute.md b/docs/phases/off-queue/53-weather-commute.md index 7fb7a91..bde550a 100644 --- a/docs/phases/off-queue/53-weather-commute.md +++ b/docs/phases/off-queue/53-weather-commute.md @@ -30,7 +30,8 @@ ## Критерий готовности -- Снежным утром класс наполняется позже, чем ясным; ленивый по-прежнему ближе к звонку +- Снежным утром `AppearAt` раньше на `commuteSnowMinutes` (запас на дорогу, не более медленная + ходьба); ленивый по-прежнему ближе к звонку - `dotnet test` проходит ## Стоп diff --git a/tests/HSchool.Ai.Tests/WalkingTests.cs b/tests/HSchool.Ai.Tests/WalkingTests.cs index d0fdd2d..8b4349f 100644 --- a/tests/HSchool.Ai.Tests/WalkingTests.cs +++ b/tests/HSchool.Ai.Tests/WalkingTests.cs @@ -162,6 +162,30 @@ public class WalkingTests Assert.True(implied.Comes); } + [Fact] + public void Lazy_AppearsCloserToTheBell_OnSnow() + { + var (catalog, map) = Fixtures.Vanilla(); + var walks = WalkGraph.Build(catalog, map); + var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", TuesdayLesson); + var lazy = roster.People.First(person => person.IsStudent && person.Traits.Contains("Lazy")); + var schoolClass = roster.Classes.First(row => row.Id == lazy.ClassId); + var keen = roster.People.First(person => + person.IsStudent && person.ClassId == lazy.ClassId && !person.Traits.Contains("Lazy")); + var table = new Timetable( + [new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1)], + []); + var extra = catalog.BehaviorRules!.CommuteSnowMinutes; + var lazyPlan = DayPlans.Build( + catalog, walks, lazy, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extra); + var keenPlan = DayPlans.Build( + catalog, walks, keen, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extra); + + Assert.True(lazyPlan.Comes); + Assert.True(keenPlan.Comes); + Assert.True(lazyPlan.AppearAt > keenPlan.AppearAt); + } + [Fact] public void SameSeedAndSnowMinutes_YieldTheSameAppearAt() { diff --git a/tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs b/tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs index 9674a28..7da7e03 100644 --- a/tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs +++ b/tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs @@ -102,6 +102,24 @@ public class LessonTeacherPresentTests } } + [Fact] + public void TeacherActionInRoom_StillGains() + { + var (school, homeroom, pupilId, teacherId) = StaffedMath(); + using (school) + { + AdvanceTo(school, LessonStart); + SetPlace(school, pupilId, homeroom); + SetPlace(school, teacherId, homeroom); + SetActivity(school, teacherId, "EatLunch"); + var before = SkillOf(school, pupilId, "Mathematics"); + + LessonLearningSystem.Apply(school, 45); + + Assert.True(SkillOf(school, pupilId, "Mathematics") > before); + } + } + [Fact] public void HungryStillGainsLess_WhenTeacherIsPresent() { @@ -221,6 +239,30 @@ public class LessonTeacherPresentTests } } + [Fact] + public void MissingWarmthNeed_GainsLikeFullWarmth() + { + var (school, homeroom, pupilId, teacherId) = StaffedMath(); + using (school) + { + AdvanceTo(school, LessonStart); + SetPlace(school, pupilId, homeroom); + SetPlace(school, teacherId, homeroom); + ClearNeed(school, pupilId, "Warmth"); + var start = SkillOf(school, pupilId, "Mathematics"); + LessonLearningSystem.Apply(school, 45); + var missingGain = SkillOf(school, pupilId, "Mathematics") - start; + + SetSkill(school, pupilId, "Mathematics", start); + SetNeed(school, pupilId, "Warmth", 1f); + LessonLearningSystem.Apply(school, 45); + var warmGain = SkillOf(school, pupilId, "Mathematics") - start; + + Assert.True(missingGain > 0); + Assert.Equal(missingGain, warmGain, precision: 5); + } + } + [Fact] public void Cold_DoesNotLog_WhenTeacherIsAbsent() { @@ -394,6 +436,20 @@ public class LessonTeacherPresentTests }); } + private static void SetActivity(School school, string personId, string actionId) + { + var query = new QueryDescription().WithAll(); + school.World.Query( + in query, + (ref PersonIdentity identity, ref PersonActivity activity) => + { + if (identity.Id.Equals(personId, StringComparison.Ordinal)) + { + activity = new PersonActivity(actionId, Thing: null, RemainingMinutes: 10f); + } + }); + } + private static void SetNeed(School school, string personId, string need, float value) { var query = new QueryDescription().WithAll(); @@ -438,6 +494,20 @@ public class LessonTeacherPresentTests }); } + private static void ClearNeed(School school, string personId, string need) + { + var query = new QueryDescription().WithAll(); + school.World.Query( + in query, + (ref PersonIdentity identity, ref PersonNeeds needs) => + { + if (identity.Id.Equals(personId, StringComparison.Ordinal)) + { + needs.Values.Remove(need); + } + }); + } + private static void ClearSkill(School school, string personId, string skill) { var query = new QueryDescription().WithAll(); diff --git a/tests/HSchool.Simulation.Tests/WeatherTests.cs b/tests/HSchool.Simulation.Tests/WeatherTests.cs index ba29180..3442a07 100644 --- a/tests/HSchool.Simulation.Tests/WeatherTests.cs +++ b/tests/HSchool.Simulation.Tests/WeatherTests.cs @@ -1,5 +1,7 @@ +using HSchool.Ai; using HSchool.Content; using HSchool.People; +using HSchool.Schedule; namespace HSchool.Simulation.Tests; @@ -73,6 +75,24 @@ public class WeatherTests Assert.Equal(0, new OutdoorWeather(-5f, Precipitation.Snow).ExtraCommuteMinutes(null)); } + [Fact] + public void SnowOnStreet_PlansAppearEarlierThanClear() + { + // Tick would SyncWeather and overwrite ForceWeather; Apply is the plan-build path. + using var clear = ComingOnTuesday(); + using var snow = ComingOnTuesday(); + clear.ForceWeather(new OutdoorWeather(8f, Precipitation.None)); + snow.ForceWeather(new OutdoorWeather(-5f, Precipitation.Snow)); + PresenceSystem.Apply(clear, 1); + PresenceSystem.Apply(snow, 1); + + var pupilId = FirstComingPupil(clear); + var extra = clear.Catalog!.BehaviorRules!.CommuteSnowMinutes; + Assert.Equal(6, extra); + Assert.True(clear.Plans[pupilId].Comes); + Assert.Equal(clear.Plans[pupilId].AppearAt!.Value.AddMinutes(-extra), snow.Plans[pupilId].AppearAt); + } + [Fact] public void SkipEmpty_SetsMondayMorningWeather_NotSaturdays() { @@ -103,6 +123,25 @@ public class WeatherTests Assert.True(room < 18f, $"walls only, got {room}"); } + private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); + + private static School ComingOnTuesday() + { + var school = Open(TuesdayMorning); + var schoolClass = school.Roster!.Classes.First(); + school.SetTimetable(new Timetable( + [new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1)], + [])); + return school; + } + + private static string FirstComingPupil(School school) + { + var pupil = school.Roster!.People.First(person => + person.IsStudent && person.ClassId == school.Timetable!.Lessons[0].ClassId); + return pupil.Id; + } + private static School Open(DateTime start) { var (catalog, map) = VanillaMap(); From 94c31c0dc1a6a04b97c7e62648ccd10c4e8c7276 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 17:13:22 +0300 Subject: [PATCH 2/2] Record off-queue lesson phases 48 and 50-53 as reviewed. Journal the teacher/skill/warmth/textbook/commute pass without closing DX or portraits. --- docs/phases/off-queue/reviewed.md | 33 ++++++++++++++++++++++++++++++- docs/phases/reviewed.md | 1 + 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/phases/off-queue/reviewed.md b/docs/phases/off-queue/reviewed.md index 4955696..116d570 100644 --- a/docs/phases/off-queue/reviewed.md +++ b/docs/phases/off-queue/reviewed.md @@ -1,6 +1,37 @@ # Проверенные фазы «Вне очереди» -Портреты 60–62. Соседей по папке (уроки, DX) этот журнал не закрывает. +Портреты 60–62 и связность урока (48, 50–53). DX (49, 54, 55) этот журнал не закрывает. + +## Связность урока. Учитель, навык, тепло, учебник, дорога + +- **Фазы:** 48, 50–53 +- **Проверен на:** `aa20534`, 2026-08-20 +- **Пути:** `src/HSchool.Ai/LessonLearning.cs`, `src/HSchool.Ai/DayPlan.cs`, `src/HSchool.Simulation/LessonLearningSystem.cs`, `src/HSchool.Simulation/OutdoorWeather.cs`, `src/HSchool.Simulation/PresenceSystem.cs`, `src/HSchool.Simulation/PersonLog.cs`, `src/HSchool.Simulation/School.cs`, `src/HSchool.Content/PeopleDefs.cs`, `src/HSchool.Content/PeopleDefValidator.cs`, `src/HSchool.Server/mods/core/defs/behavior/rules.jsonc`, `src/HSchool.Server/mods/core/localizations/ru.jsonc`, `src/HSchool.Server/mods/core/localizations/en.jsonc`, `tests/HSchool.Ai.Tests/LessonLearningTests.cs`, `tests/HSchool.Ai.Tests/WalkingTests.cs`, `tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs`, `tests/HSchool.Simulation.Tests/WeatherTests.cs`, `tests/HSchool.Content.Tests/BehaviorDefTests.cs`, `docs/protocol.md`, `docs/design/off-queue/lesson-consequences.md`, `docs/design/07-inventory/inventory.md` +- **Итог:** обещания фаз 48 и 50–53 совпадают с кодом; сокет не бампили (`ProtocolConstants.Version` / `PROTOCOL_VERSION` = 9). Дописаны тесты на действие учителя, тепло без нужды, снег в плане дня и ленивого на снегу. Критерий фазы 53 «класс наполняется позже» поправлен на `AppearAt` раньше (запас на дорогу). DX 49/54/55 и портреты не трогались. + +### Фаза 48 + +Рост только если назначенный учитель стоит в `lesson.RoomId` (путь пуст, остаток 0). Нет id в мире — как отсутствующий. Лог `lesson-no-teacher` один раз на предмет в сутки, `thingDef` — предмет, подпись из каталога. Голод по-прежнему режет, когда учитель на месте. Действие учителя урок не отменяет — это проверяет `TeacherActionInRoom_StillGains`. + +### Фаза 50 + +`TeacherFactor` — та же кривая, что `NeedFactor` (0 → 0.25, 100 → 1). Навык — среднее по `SubjectDef.skills`, нет ключа — `Range.Min`. Учителя нет в кабинете — рост нулевой. + +### Фаза 51 + +`Gain` умножает `NeedFactor(голод) × NeedFactor(тепло)`; нет ключа `Warmth` — как 1. Лог `lesson-cold` один раз на предмет, только если учитель в кабинете. В `inventory.md` штраф рядом с голодом. + +### Фаза 52 + +Полный рост только при учебнике этого предмета в сумке; шкафчик и дом — `lessonNoTextbookFactor` (ваниль 0.5). Пак без поля — 0.5. Лог `lesson-no-textbook` один раз на предмет. + +### Фаза 53 + +`DayPlans.Build` берёт уже посчитанные минуты; симуляция подставляет `OutdoorWeather.ExtraCommuteMinutes`. Ваниль 3 / 6; пак без полей — 0. Лога нет. `HSchool.Ai` не знает `Precipitation`. + +### Открытое + +Планы дня не пересобираются, если осадки сменились после первой сборки этого утра — так записано («в момент сборки»). Учитель, стоящий в своём кабинете, тоже получает `Gain` по скиллам предмета (`Duty.LessonsToday` для штата); фазы это не запрещают. ## Портреты. Модели, слои промпта, LoRA и embeddings diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 2be9336..a7d5d08 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -33,5 +33,6 @@ | Срез 9. Этапы C–D — конфликт, присутствие, речь и romance | `19554d4` | [09-social](09-social/reviewed.md) | | Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) | | Срез 10. Этапы B–C — работник, карточка и дамп | `f4f726a` | [10-craft](10-craft/reviewed.md) | +| Связность урока (вне очереди) 48, 50–53 | `aa20534` | [off-queue](off-queue/reviewed.md) | | Портреты (вне очереди) 60–61 | `5e23d1f` | [off-queue](off-queue/reviewed.md) | | Портреты (вне очереди) 62 | `d093ea3` | [off-queue](off-queue/reviewed.md) |