Update reviewed phase details, enhance presence tests for hiring and assigning teachers during lessons, and fix timetable handling in the School class. Added new tests for presence management and ensured proper campus staffing behavior during lesson transitions.
This commit is contained in:
+52
-5
@@ -519,15 +519,16 @@ Calendar+обеды 20, планировщик 9, TimetableApi 6.
|
|||||||
## Срез 5. Жизнь школы
|
## Срез 5. Жизнь школы
|
||||||
|
|
||||||
- **Фазы:** 18–21
|
- **Фазы:** 18–21
|
||||||
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `21d79cb`). Фаза 21 закрыта за час до проверки, `core` правился
|
- **Проверен на:** `450495f`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`)
|
||||||
параллельно — проверка велась в отдельном worktree на коммите, не на рабочем дереве
|
|
||||||
- **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`,
|
- **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`,
|
||||||
`src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов),
|
`src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов),
|
||||||
`src/HSchool.Server/Game/PresenceFrame.cs`, `src/HSchool.Client/src/ui/gameScreen.ts`,
|
`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`,
|
`src/HSchool.Server/mods/core/defs/{actions,behavior,needs}`, `tests/HSchool.Ai.Tests`,
|
||||||
`tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests}.cs`
|
`tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests,LunchTests}.cs`,
|
||||||
- **Итог:** все 32 теста из списков четырёх фаз на месте и проходят; тестов не дописано, зато
|
`tests/HSchool.AppHost.Tests/GameSocketTests.cs`
|
||||||
измерен прожитый учебный день — и он показал две вещи, которых списки не ловят
|
- **Итог:** все 32 теста из списков четырёх фаз на месте; дописаны 2 теста явки после смены таблицы,
|
||||||
|
`SetTimetable` сбрасывает планы дня, хостовый тест ждёт `Clock.Running` после снятия паузы.
|
||||||
|
Голод из первого прохода уже закрыт данными. AppHost не гонялся — Aspire держит DLL.
|
||||||
|
|
||||||
Что подтверждено:
|
Что подтверждено:
|
||||||
|
|
||||||
@@ -606,6 +607,52 @@ Calendar+обеды 20, планировщик 9, TimetableApi 6.
|
|||||||
ходьба, действия и решения не тронуты, `decayPerHour` и `needGain` тоже, поэтому измеренная выше
|
ходьба, действия и решения не тронуты, `decayPerHour` и `needGain` тоже, поэтому измеренная выше
|
||||||
картина с голодом остаётся в силе: вопрос «чем закрывать голод» по-прежнему открыт.
|
картина с голодом остаётся в силе: вопрос «чем закрывать голод» по-прежнему открыт.
|
||||||
|
|
||||||
|
### Перепроверка на `450495f`, 2026-08-20
|
||||||
|
|
||||||
|
Дрейф после `5a00ad7` — обеденные смены, веса в `BehaviorDef` (фаза 24), `Decision.cs`,
|
||||||
|
`PresenceSystem` (очередь после действий). Обещания фаз 18–21 целы; протокол остался v7.
|
||||||
|
|
||||||
|
Что подтверждено повторно:
|
||||||
|
|
||||||
|
- **Фаза 18.** Все 13 тестов списка на месте в `WalkingTests` / `PresenceTests`. Пустая школа
|
||||||
|
без учителей в рабочее окно не пропускается. `EnsurePlans` пересобирал планы только при смене
|
||||||
|
дня или численности — после пина или назначения предмета без найма `AppearAt` оставался `null`.
|
||||||
|
- **Фаза 19.** Раскладка присутствия с обеих сторон (`Presence_RoundTrips`, `protocol.test.ts`),
|
||||||
|
снимок карты без людей, пропуск в учебное время отбрасывается. Кадр часов несёт разрешение на
|
||||||
|
пропуск; клиент его не считает.
|
||||||
|
- **Фаза 20.** Декей голода, обед, стулья, ночь вне школы, неизвестная нужда, детерминизм.
|
||||||
|
`SleepAndHunger_ReturnToMaxOffCampus` и `AfterASchoolDay_NobodyIsLeftStarving` закрывают
|
||||||
|
открытый в первом проходе голод.
|
||||||
|
- **Фаза 21.** Восемь тестов списка плюс обеденные веса в `DecisionPlannerTests`. `HSchool.Ai`
|
||||||
|
без Arch/ASP.NET/`DateTime.Now`, `Simulation` без сокетов.
|
||||||
|
|
||||||
|
Исправлено:
|
||||||
|
|
||||||
|
- `School.SetTimetable` обнуляет `PlanDay`. Иначе `EnsurePlans` считает планы актуальными,
|
||||||
|
пока не сменятся день или численность, и после назначения математики в 10:20 никто не приходит:
|
||||||
|
вчерашний план собран по пустой таблице, `AppearAt == null`. Найм это маскировал (численность
|
||||||
|
растёт), пин и назначение — нет.
|
||||||
|
- `OpeningASchoolDuringAMathLesson_...` после снятия паузы сразу ждал людей в кадре. Присутствие
|
||||||
|
уходит и на паузе (пустые люди, подписи урока из таблицы), и 40 кадров бюджета уходили в эти
|
||||||
|
кадры до обработки `SetRunning`. Теперь тест ждёт `clock.Running`, потом людей. Сам хост
|
||||||
|
с Aspire в этой сессии не гонялся (MSB3021).
|
||||||
|
|
||||||
|
Дописано:
|
||||||
|
|
||||||
|
- `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus` — найм на паузе в 10:20, потолок
|
||||||
|
64 решения, шаг 20 Гц, как у работника.
|
||||||
|
- `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus` — назначение без смены численности;
|
||||||
|
без сброса `PlanDay` кампус остаётся пустым.
|
||||||
|
|
||||||
|
Закрыто из прошлого прохода:
|
||||||
|
|
||||||
|
- **Голод.** Уже закрыто 2026-08-19 (`restoredOffCampus`, стулья 112, смены). Повторно зелёные
|
||||||
|
`NeedDecayTests.SleepAndHunger_ReturnToMaxOffCampus` и `LunchTests.AfterASchoolDay_NobodyIsLeftStarving`.
|
||||||
|
- **`OpeningASchoolDuringAMathLesson_...`.** Это была явка плюс чтение паузных кадров, не флак
|
||||||
|
нагрузки. Симуляция после правки зелёная; хостовый прогон — когда Aspire отпустят DLL.
|
||||||
|
|
||||||
|
Ai 29, Presence+Decision+Activity+NeedDecay+Lunch 31, Presence_RoundTrips 1.
|
||||||
|
|
||||||
## Что из журнала закрыто, 2026-08-19
|
## Что из журнала закрыто, 2026-08-19
|
||||||
|
|
||||||
Ревью нашло семь пунктов; шесть закрыты в этот же день, седьмой снят как несуществующий.
|
Ревью нашло семь пунктов; шесть закрыты в этот же день, седьмой снят как несуществующий.
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ public sealed class School : IDisposable
|
|||||||
ArgumentNullException.ThrowIfNull(timetable);
|
ArgumentNullException.ThrowIfNull(timetable);
|
||||||
Timetable = timetable;
|
Timetable = timetable;
|
||||||
TimetableDirty = false;
|
TimetableDirty = false;
|
||||||
|
// Duty rooms and AppearAt come from this table. EnsurePlans only rebuilds when the day
|
||||||
|
// or the roster size changes, so a pin or an assign would otherwise keep yesterday's
|
||||||
|
// empty-staff plans and leave the campus empty.
|
||||||
|
PlanDay = null;
|
||||||
LastDecisionSlot = null;
|
LastDecisionSlot = null;
|
||||||
foreach (var id in Roster?.People.Select(person => person.Id) ?? [])
|
foreach (var id in Roster?.People.Select(person => person.Id) ?? [])
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ public class GameSocketTests(AppHostFixture fixture)
|
|||||||
// one person made this test fail under load.
|
// one person made this test fail under load.
|
||||||
await SendAsync(socket, buffer =>
|
await SendAsync(socket, buffer =>
|
||||||
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: true)));
|
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: true)));
|
||||||
|
// Presence still goes out while paused (empty people, lesson labels from the table).
|
||||||
|
// Waiting for people first drains those frames and can spend the 40-frame budget
|
||||||
|
// before the resume even lands. Clock.Running is the signal that time is moving.
|
||||||
|
await ReceiveClockWhereAsync(socket, clock => clock.Running);
|
||||||
presence = await ReceivePresenceWhereAsync(socket, frame => frame.People.Count > 0);
|
presence = await ReceivePresenceWhereAsync(socket, frame => frame.People.Count > 0);
|
||||||
Assert.All(presence.People, person => Assert.False(string.IsNullOrWhiteSpace(person.NodeId)));
|
Assert.All(presence.People, person => Assert.False(string.IsNullOrWhiteSpace(person.NodeId)));
|
||||||
Assert.Contains(presence.People, person => directory.People.Any(row => row.Id == person.Id));
|
Assert.Contains(presence.People, person => directory.People.Any(row => row.Id == person.Id));
|
||||||
|
|||||||
@@ -224,11 +224,87 @@ public class PresenceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus()
|
public void HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus()
|
||||||
{
|
{
|
||||||
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
|
using var school = UnstaffedDuringLessonThree();
|
||||||
using var school = OpenEmpty(start);
|
HireMathTeacher(school);
|
||||||
school.Tick(0.2d, 5d);
|
Assert.True(school.IsCampusEmpty(), "paused ticks must not walk anyone in");
|
||||||
Assert.True(school.IsCampusEmpty());
|
|
||||||
|
|
||||||
|
school.Clock.IsRunning = true;
|
||||||
|
Assert.True(SomeoneWalksIn(school), $"nobody on campus after mid-lesson hire at {school.Clock.Time:HH:mm}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus()
|
||||||
|
{
|
||||||
|
using var school = UnstaffedDuringLessonThree();
|
||||||
|
var catalog = school.Catalog!;
|
||||||
|
var map = school.Map!;
|
||||||
|
var hired = Staffing.Hire(
|
||||||
|
catalog,
|
||||||
|
map,
|
||||||
|
school.Roster!,
|
||||||
|
school.Applicants!,
|
||||||
|
school.Applicants!.Applicants[0].Person.Id,
|
||||||
|
Staffing.TeacherPosition,
|
||||||
|
Cap);
|
||||||
|
Assert.Equal(StaffingError.None, hired.Error);
|
||||||
|
school.ApplyStaffing(hired.Roster, hired.Pool);
|
||||||
|
school.SetTimetable(SchoolTimetables.Build(catalog, map, hired.Roster, null, weekDays: 5));
|
||||||
|
|
||||||
|
school.Clock.IsRunning = true;
|
||||||
|
school.Tick(1d / 20d, 5d);
|
||||||
|
school.Clock.IsRunning = false;
|
||||||
|
Assert.True(school.IsCampusEmpty(), "a teacher with no subjects still has no reason to come");
|
||||||
|
|
||||||
|
var assigned = Staffing.AssignSubject(
|
||||||
|
catalog,
|
||||||
|
hired.Roster,
|
||||||
|
hired.Pool,
|
||||||
|
hired.Roster.People.Single(person => person.IsStaff).Id,
|
||||||
|
"Mathematics",
|
||||||
|
Cap);
|
||||||
|
Assert.Equal(StaffingError.None, assigned.Error);
|
||||||
|
school.ApplyStaffing(assigned.Roster, assigned.Pool);
|
||||||
|
school.SetTimetable(SchoolTimetables.Build(catalog, map, assigned.Roster, null, weekDays: 5));
|
||||||
|
|
||||||
|
school.Clock.IsRunning = true;
|
||||||
|
Assert.True(
|
||||||
|
SomeoneWalksIn(school),
|
||||||
|
"stale day plans after an assign that did not change headcount left the campus empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Simulation_DoesNotReferenceSockets()
|
||||||
|
{
|
||||||
|
var names = typeof(School).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
|
||||||
|
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static School UnstaffedDuringLessonThree()
|
||||||
|
{
|
||||||
|
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
|
||||||
|
var school = OpenEmpty(start);
|
||||||
|
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 64);
|
||||||
|
school.SetTimetable(SchoolTimetables.Build(school.Catalog!, school.Map!, school.Roster!, null, weekDays: 5));
|
||||||
|
|
||||||
|
var step = 1d / 20d;
|
||||||
|
for (var i = 0; i < 40; i++)
|
||||||
|
{
|
||||||
|
school.Tick(step, 5d);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(school.IsCampusEmpty());
|
||||||
|
school.Clock.IsRunning = false;
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
school.Tick(step, 5d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return school;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HireMathTeacher(School school)
|
||||||
|
{
|
||||||
var catalog = school.Catalog!;
|
var catalog = school.Catalog!;
|
||||||
var map = school.Map!;
|
var map = school.Map!;
|
||||||
var hired = Staffing.Hire(
|
var hired = Staffing.Hire(
|
||||||
@@ -242,25 +318,31 @@ public class PresenceTests
|
|||||||
Assert.Equal(StaffingError.None, hired.Error);
|
Assert.Equal(StaffingError.None, hired.Error);
|
||||||
school.ApplyStaffing(hired.Roster, hired.Pool);
|
school.ApplyStaffing(hired.Roster, hired.Pool);
|
||||||
|
|
||||||
var assigned = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, hired.Roster.People.Single(person => person.IsStaff).Id, "Mathematics", Cap);
|
var assigned = Staffing.AssignSubject(
|
||||||
|
catalog,
|
||||||
|
hired.Roster,
|
||||||
|
hired.Pool,
|
||||||
|
hired.Roster.People.Single(person => person.IsStaff).Id,
|
||||||
|
"Mathematics",
|
||||||
|
Cap);
|
||||||
Assert.Equal(StaffingError.None, assigned.Error);
|
Assert.Equal(StaffingError.None, assigned.Error);
|
||||||
school.ApplyStaffing(assigned.Roster, assigned.Pool);
|
school.ApplyStaffing(assigned.Roster, assigned.Pool);
|
||||||
school.SetTimetable(SchoolTimetables.Build(catalog, map, assigned.Roster, null, weekDays: 5));
|
school.SetTimetable(SchoolTimetables.Build(catalog, map, assigned.Roster, null, weekDays: 5));
|
||||||
|
|
||||||
for (var i = 0; i < 20; i++)
|
|
||||||
{
|
|
||||||
school.Tick(0.2d, 5d);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Contains(school.CapturePresence(), row => row.NodeId is not null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
private static bool SomeoneWalksIn(School school)
|
||||||
public void Simulation_DoesNotReferenceSockets()
|
|
||||||
{
|
{
|
||||||
var names = typeof(School).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
|
var step = 1d / 20d;
|
||||||
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
|
for (var i = 0; i < 400; i++)
|
||||||
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
|
{
|
||||||
|
school.Tick(step, 5d);
|
||||||
|
if (school.CapturePresence().Any(row => row.NodeId is not null))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (School School, string Homeroom, string PupilId) StaffedFirstFloorClass()
|
private static (School School, string Homeroom, string PupilId) StaffedFirstFloorClass()
|
||||||
|
|||||||
Reference in New Issue
Block a user