Enhance staffing management with uncovered teacher tracking
ci / server (push) Failing after 3m43s
ci / client (push) Successful in 14s

- Updated `protocol.md` to clarify the concept of uncovered subjects and the number of additional teachers required.
- Introduced `teachersShort` property in the `StaffingSubject` interface to indicate how many more teachers are needed for a subject.
- Enhanced localization strings to reflect the new uncovered teacher information in both English and Russian.
- Updated the management panel UI to display the number of teachers short for each uncovered subject.
- Revised the `Uncovered` method in the staffing logic to return detailed information about uncovered subjects, including the shortfall of teachers.
- Added tests to validate the new uncovered teacher tracking functionality and ensure accurate reporting in the staffing API.
- Marked related tasks as complete in the documentation for the golden fixtures phase.
This commit is contained in:
Leonid Pershin
2026-08-19 23:53:37 +03:00
parent c27d43f66a
commit e5a0ae5b9d
27 changed files with 16055 additions and 38 deletions
+185
View File
@@ -0,0 +1,185 @@
---
name: phase-work
description: Берёт ровно одну фазу из среза в docs/phases/ и доводит её до конца — в своём git worktree и на своей ветке, чтобы над одним срезом можно было запустить несколько агентов параллельно. Используй, когда просят «сделай фазу 25», «возьми фазу», «реализуй фазу», «начни срез», «раздели срез между агентами», «work on the phase». Не для ревью уже написанного — там /phase-review, и не для правки одного файла по просьбе.
---
# Работа над фазой
Один запуск — **одна фаза**. Скилл берёт её из `docs/phases/`, делает целиком, проверяет по её же
критериям приёмки и отдаёт готовую ветку. Ничего сверх взятой фазы он не трогает: именно это
позволяет запустить несколько агентов на один срез и не получить кашу.
Критерии приёмки уже написаны. У каждой фазы есть «Задачи», «Тесты, без которых фаза не закрыта» и
«Критерий готовности», у проекта — инварианты в `AGENTS.md`, у среза — дизайн-док. Выдумывать
нечего, надо сделать и доказать.
## Какую фазу брать
**Номер передали в аргументе** — берёшь его, вопросов нет. Так пользователь раздаёт фазы агентам:
`/phase-work 25`, `/phase-work 26` в соседних окнах.
**Номера нет** — выбираешь сам:
1. Читаешь `docs/phases/README.md`: находишь первый срез, где есть ⬜.
2. Внутри среза берёшь первую ⬜, у которой закрыты зависимости — они перечислены в шапке фазы.
3. Проверяешь, что её никто не взял: `git branch --list "phase/<номер>-*"` пусто.
**Статус в `README.md` — первый источник правды, ветка — только арбитр гонки.** 🔄 значит занято,
✅ значит сделано, ⬜ значит свободно. Ветка может существовать и при ✅ — её просто не убрали после
слияния; это не заявка, а мусор, см. «Кто убирает ветку».
Фаза 🔄 — чужая работа в процессе. Не трогай её, даже если кажется, что там застряли.
## Как застолбить
Ветка — и есть заявка, потому что git не даст создать её дважды:
```bash
git branch phase/25-golden-fixtures
```
Команда упала с «already exists» — значит фазу взял другой агент между твоей проверкой и попыткой.
Не спорь, вернись к выбору и возьми следующую.
Ветка создалась — фаза твоя. Сразу пометь её 🔄 в `docs/phases/README.md` **в основном дереве**,
отдельным коммитом, состоящим только из этой строки. Это единственная правка, которую ты делаешь
вне своего worktree, и она нужна по двум причинам: соседние агенты видят занятое, а пустая свежая
ветка ещё ничем не отличается от слитой — по ней одной понять, что фаза в работе, нельзя.
Пометить 🔄 надо **до** первой строчки кода, а не после.
## Где работать
В своём worktree, а не в общем дереве. Иначе два агента правят одни файлы и роняют друг другу
сборку.
```bash
git worktree add <путь-в-scratchpad>/wt-phase-25 phase/25-golden-fixtures
```
Путь — **вне репозитория**; каталог scratchpad этой сессии подходит. Worktree внутри репозитория
засоряет `git status` и попадёт в глаза следующему ревью.
Дальше вся работа там. В конце worktree удаляется (`git worktree remove --force`), ветка остаётся.
Что нужно знать про worktree:
- Первая сборка полная, минуту-две. Это нормально, а не сломанный кэш.
- `bin` и `obj` у каждого worktree свои, поэтому параллельные `dotnet test` не мешают друг другу.
- Тесты хоста поднимают сервер со **своей** папкой сейвов внутри worktree — соседний агент их не
видит. Порты Aspire раздаёт сам, конфликтов нет.
- `node_modules` не копируется. Для клиентской фазы либо `npm ci`, либо скопируй `node_modules` из
основного дерева — это быстрее и достаточно.
## Порядок работы
**1. Прочитай три документа, прежде чем писать код.** Саму фазу; дизайн-док среза, ссылка на него
в шапке среза в `README.md`; инварианты и «Things that will bite you» в `AGENTS.md`. Дизайн
объясняет *почему* так, а не иначе — без него легко сделать формально по задачам и мимо смысла.
**2. Начни с тестов из списка «Тесты, без которых фаза не закрыта».** Они уже сформулированы —
превращай каждый пункт в тест. Проект для теста выбирает не удобство, а «Testing policy» в
`AGENTS.md`: генерация людей — в `HSchool.People.Tests`, планировщик — в `HSchool.Schedule.Tests`,
эндпоинты — в `HSchool.AppHost.Tests`. Тест генератора в тестах хоста найдёт следующее ревью.
**3. Делай задачи по порядку и отмечай `- [x]` по мере готовности.** Галочка ставится, когда есть
код **и** подтверждение, а не когда «в целом написано».
**4. Правь документы тем же коммитом, что и код.** Это правило проекта, а не вежливость:
- меняется раскладка сообщения — `ProtocolCodec.cs`, `protocol.ts` и `docs/protocol.md` вместе,
плюс бамп версии;
- меняется HTTP — `docs/protocol.md` тем же коммитом;
- решение разошлось с дизайн-доком — правится документ, и в отчёте это названо явно.
**5. Проверь то, что заявляешь.** Прогоняешь тесты своей фазы и полный прогон. Не «должно
работать», а «прогнал, вот результат».
## Что считать сделанным
Фаза закрыта, когда **каждый** пункт «Тестов, без которых фаза не закрыта» существует отдельным
тестом и проходит, и когда выполнен «Критерий готовности». Не раньше.
✅ в `README.md` ставится последним коммитом ветки, и в этом коммите — только строка статуса.
Отдельным потому, что при слиянии нескольких веток строки статуса конфликтуют, а мелкий коммит
разруливается за секунду.
Не смог закрыть пункт — оставь ⬜ и напиши в отчёте, что именно не вышло и почему. Полработы с
честной пометкой лучше, чем ✅ с дырой: следующее ревью её всё равно найдёт, только дороже.
## Кто убирает ветку
Ты — нет. Ветка это твой результат: пока её не слили, удалять нечего.
Порядок такой:
1. Ты убираешь **worktree** — сразу, как закончил: `git worktree remove --force <путь>`. Пока
worktree держит ветку, её нельзя ни слить нормально, ни удалить.
2. Пользователь сливает ветку и удаляет её: `git branch -d phase/25-golden-fixtures`. С `-d`, а не
`-D`: git откажется, если что-то не слито, и это правильная защита.
3. Ветку забыли удалить — её уберёт следующий запуск скилла, но **только при двух условиях сразу**:
фаза стоит ✅ в `README.md` **и** ветка слита в `main`.
```bash
git branch --merged main --list "phase/*" --format="%(refname:short)"
```
Оба условия обязательны, и вот почему: только что созданная ветка ещё не имеет коммитов, а значит
равна `main` и в этот список попадает. Удалить её по одной «слитости» — снести чужую только что
взятую фазу. Статус ✅ и есть то, что отличает доделанное от начатого.
Осиротевшие worktree (агент умер, каталог остался) подчищаются `git worktree prune` — это
безопасно, записи о несуществующих каталогах и так мусор.
## Границы
- **Только своя фаза.** Заметил проблему в соседней — строкой в отчёт, не в диф. Реализация,
которая походя переписала полпроекта, невозможна к просмотру глазами.
- **Не сливай ветку.** Слияние и порядок веток — решение пользователя, у него их несколько.
- **Не переставляй чужие статусы** в `README.md` и не трогай чужие 🔄.
- **Не меняй молча** версию протокола, форму сейва и публичное поведение API — даже когда это
очевидно правильно. Если фаза этого требует, так и написано в её задачах.
- **Общие файлы трогай минимально.** `AGENTS.md`, `docs/protocol.md`, `README.md` правят все ветки
сразу — чем точечнее правка, тем дешевле слияние.
## Окружение: что укусит
- **`MSB3021` / `MSB3027`, «блокирует этот файл»** — у пользователя запущено приложение, оно держит
DLL. **Процесс не убивать.** В своём worktree сборка обычно проходит, там свои `bin`; если всё же
упёрлось — проверь то, что можно без сборки, и скажи об этом в отчёте прямо.
- **Концы строк.** В репозитории есть и CRLF, и LF. `sed -i` переворачивает файл целиком: правка в
три строки превращается в диф на тысячу. Правь скриптом, который читает и пишет с `newline`
пустой строкой, а потом смотри `git diff --stat` — внезапные сотни строк там, где ты правил три,
это оно.
- **Heredoc и экранирование.** Файлы с табуляциями, юникод-экранами и кавычками приезжают через
heredoc покалеченными. Пиши такие файлы инструментом записи файла, а не `cat` в шелле.
- **Тесты хоста делят один сервер и одну папку сейвов**, поэтому каждый начинает с очистки списка.
Не пиши тест, который полагается на пустую базу или на конкретный id школы.
- **Состав школы зависит от её id**, пока не сделана фаза 26. Тест, требующий у первого ученика
двух родителей или конкретную фамилию, зелёный у тебя и красный в CI. Проверяй свойство, а не
совпадение.
- **Полный `dotnet test` изредка падает целиком** с нативной ошибкой в `Simulation.Tests`. Один раз
— перепрогони; повторяется — это находка для отчёта.
- **Dev-сервер не запускать.** Нужно посмотреть на UI — пользуйся уже запущенным приложением
пользователя.
## Измеряй, а не рассуждай
Когда вопрос звучит как «а хорошо ли ложатся часы» или «а сколько выходит неполных семей» — не
рассуждай о коде. Напиши временный тест, который печатает реальные числа, посмотри и удали его.
Час размышлений о поведении алгоритма стоит дороже и ошибается чаще, чем один прогон с цифрами.
Числа из такого замера — лучшее, что можно положить в отчёт.
## Отчёт
Коротко, в конце:
- какая фаза взята и на какой ветке лежит;
- что сделано — по пунктам задач, без пересказа кода;
- какие тесты добавлены, списком по одной строке;
- чем подтверждено: что прогнал и с каким результатом, числа замеров, если были;
- что осталось незакрытым и почему;
- что замечено за границами фазы.
Ветку не сливаешь и не удаляешь — она и есть результат. Worktree за собой убираешь, иначе ветку
не получится ни слить, ни удалить.
+14 -14
View File
@@ -11,26 +11,26 @@
## Задачи ## Задачи
- [ ] Отпечаток ростера файлом в фикстурах: фиксированные сид, карта, набор имён и родной язык - [x] Отпечаток ростера файлом в фикстурах: фиксированные сид, карта, набор имён и родной язык
- [ ] Тест печатает **первую** несовпавшую строку, а не «строки различаются» - [x] Тест печатает **первую** несовпавшую строку, а не «строки различаются»
- [ ] Правило записано рядом с фикстурой: отпечаток обновляется в том же коммите, что и генерация, - [x] Правило записано рядом с фикстурой: отпечаток обновляется в том же коммите, что и генерация,
и коммит объясняет, что поменялось и коммит объясняет, что поменялось
- [ ] Файл сейва текущего формата в фикстурах хоста; тест поднимает из него школу - [x] Файл сейва текущего формата в фикстурах хоста; тест поднимает из него школу
- [ ] Рядом с ним — файл сейва **без** новых полей, чтобы «отсутствующее поле имеет разумное - [x] Рядом с ним — файл сейва **без** новых полей, чтобы «отсутствующее поле имеет разумное
значение» проверялось, а не подразумевалось значение» проверялось, а не подразумевалось
- [ ] Форма голодного дня закрепляется тестом: приходят сытыми, к своей смене около 0.4, уходят - [x] Форма голодного дня закрепляется тестом: приходят сытыми, к своей смене около 0.4, уходят
голодными, к утру снова полны. Числа подобраны замером и сейчас ничем не защищены голодными, к утру снова полны. Числа подобраны замером и сейчас ничем не защищены
- [ ] `uncovered` в staffing говорит, скольких учителей не хватает предмету - [x] `uncovered` в staffing говорит, скольких учителей не хватает предмету
- [ ] Клиент показывает это число в списке непокрытых, через `t(...)` - [x] Клиент показывает это число в списке непокрытых, через `t(...)`
## Тесты, без которых фаза не закрыта ## Тесты, без которых фаза не закрыта
- [ ] Отпечаток ростера совпадает с файлом - [x] Отпечаток ростера совпадает с файлом
- [ ] Школа поднимается из сохранённого файла: те же люди, то же время, тот же штат - [x] Школа поднимается из сохранённого файла: те же люди, то же время, тот же штат
- [ ] Сейв без поля родного языка грузится и не перетасовывает набор - [x] Сейв без поля родного языка грузится и не перетасовывает набор
- [ ] Кривая голода за учебный день держится в заявленных границах - [x] Кривая голода за учебный день держится в заявленных границах
- [ ] `PrimarySchool` на ванильной карте требует трёх учителей, и ответ это говорит - [x] `PrimarySchool` на ванильной карте требует трёх учителей, и ответ это говорит
- [ ] Назначение второго учителя уменьшает нехватку, но не закрывает её - [x] Назначение второго учителя уменьшает нехватку, но не закрывает её
## Критерий готовности ## Критерий готовности
+1 -1
View File
@@ -109,7 +109,7 @@
| [22. Удостоверение пака](22-mod-identity.md) | ⬜ | Название, версия, зависимости и порядок загрузки | | [22. Удостоверение пака](22-mod-identity.md) | ⬜ | Название, версия, зависимости и порядок загрузки |
| [23. Пример мода](23-example-pack.md) | ⬜ | Настоящая папка вместо документов в памяти | | [23. Пример мода](23-example-pack.md) | ⬜ | Настоящая папка вместо документов в памяти |
| [24. Числа поведения](24-behavior-numbers.md) | ⬜ | Веса целей в `BehaviorDef`, а не в коде | | [24. Числа поведения](24-behavior-numbers.md) | ⬜ | Веса целей в `BehaviorDef`, а не в коде |
| [25. Золотые файлы](25-golden-fixtures.md) | | Отпечаток ростера, старый сейв, кривая голода, нехватка учителей | | [25. Золотые файлы](25-golden-fixtures.md) | | Отпечаток ростера, старый сейв, кривая голода, нехватка учителей |
| [26. Свой сид](26-school-seed.md) | ⬜ | Состав школы перестаёт зависеть от порядка создания | | [26. Свой сид](26-school-seed.md) | ⬜ | Состав школы перестаёт зависеть от порядка создания |
| [27. Версия сейва и дамп](27-serviceability.md) | ⬜ | Формат читается, внутрь школы можно заглянуть | | [27. Версия сейва и дамп](27-serviceability.md) | ⬜ | Формат читается, внутрь школы можно заглянуть |
| [28. Тесты экранов](28-screen-tests.md) | ⬜ | DOM-окружение и логика клиента под тестом | | [28. Тесты экранов](28-screen-tests.md) | ⬜ | DOM-окружение и логика клиента под тестом |
+5 -2
View File
@@ -276,7 +276,9 @@ teaching it. So a second teacher of a subject halves what the first one carries
A subject appears in `uncovered` when nobody teaches it **or** when the people who do cannot A subject appears in `uncovered` when nobody teaches it **or** when the people who do cannot
between them carry its hours (`maxWeeklyHours` each). Both mean the same thing to a player: between them carry its hours (`maxWeeklyHours` each). Both mean the same thing to a player:
those lessons will not happen. those lessons will not happen. `teachersShort` is how many more people that subject still
needs — primary school on a vanilla map is three, and hiring the second teacher leaves it at
one.
```json ```json
{ {
@@ -289,7 +291,8 @@ those lessons will not happen.
"label": "Математика", "label": "Математика",
"gradeMin": 5, "gradeMin": 5,
"gradeMax": 11, "gradeMax": 11,
"hoursPerWeek": 5 "hoursPerWeek": 5,
"teachersShort": 1
} }
], ],
"applicants": [ "applicants": [
@@ -24,6 +24,8 @@ describe('t', () => {
expect(t('peoplePager', { page: 2, pages: 10, total: 512 })).toBe('Page 2 of 10 · 512'); expect(t('peoplePager', { page: 2, pages: 10, total: 512 })).toBe('Page 2 of 10 · 512');
expect(t('staffErrorPayroll', { allocated: '10 000', payroll: '8 000', remaining: '2 000', attempted: '12 000' })) expect(t('staffErrorPayroll', { allocated: '10 000', payroll: '8 000', remaining: '2 000', attempted: '12 000' }))
.toBe('Not enough money: 8 000 of 10 000 is committed, 2 000 free, 12 000 needed.'); .toBe('Not enough money: 8 000 of 10 000 is committed, 2 000 free, 12 000 needed.');
expect(t('staffUncoveredShort', { label: 'Начальные классы', min: 1, max: 4, n: 3 }))
.toBe('Начальные классы (1–4) — 3 short');
expect(t('mapOccupancy', { name: 'Кабинет 204', activity: 'Математика · 5Б' })) expect(t('mapOccupancy', { name: 'Кабинет 204', activity: 'Математика · 5Б' }))
.toBe('Кабинет 204 (Математика · 5Б)'); .toBe('Кабинет 204 (Математика · 5Б)');
expect(t('mapHeadcount', { name: 'Коридор', count: 12 })).toBe('Коридор (12)'); expect(t('mapHeadcount', { name: 'Коридор', count: 12 })).toBe('Коридор (12)');
+2
View File
@@ -159,6 +159,7 @@ const ru = {
staffColPay: 'В месяц', staffColPay: 'В месяц',
staffAsk: '{hourly}/ч · {monthly}/мес', staffAsk: '{hourly}/ч · {monthly}/мес',
staffSubjectRange: '{label} ({min}{max})', staffSubjectRange: '{label} ({min}{max})',
staffUncoveredShort: '{label} ({min}{max}) — не хватает {n}',
staffParent: 'родитель', staffParent: 'родитель',
staffPickHint: 'Выберите сотрудника в списке или откройте соискателей.', staffPickHint: 'Выберите сотрудника в списке или откройте соискателей.',
staffHireTitle: 'Наём', staffHireTitle: 'Наём',
@@ -369,6 +370,7 @@ const en: Messages = {
staffColPay: 'Monthly', staffColPay: 'Monthly',
staffAsk: '{hourly}/h · {monthly}/mo', staffAsk: '{hourly}/h · {monthly}/mo',
staffSubjectRange: '{label} ({min}{max})', staffSubjectRange: '{label} ({min}{max})',
staffUncoveredShort: '{label} ({min}{max}) — {n} short',
staffParent: 'parent', staffParent: 'parent',
staffPickHint: 'Select a staff member in the list, or open the applicants.', staffPickHint: 'Select a staff member in the list, or open the applicants.',
staffHireTitle: 'Hire', staffHireTitle: 'Hire',
+1
View File
@@ -321,6 +321,7 @@ export interface StaffingSubject {
readonly gradeMin: number; readonly gradeMin: number;
readonly gradeMax: number; readonly gradeMax: number;
readonly hoursPerWeek: number; readonly hoursPerWeek: number;
readonly teachersShort: number;
} }
export interface StaffingApplicant { export interface StaffingApplicant {
+2 -1
View File
@@ -257,10 +257,11 @@ export class ManagementPanel {
this.uncovered.append( this.uncovered.append(
el('span', { el('span', {
class: 'people__tag', class: 'people__tag',
text: t('staffSubjectRange', { text: t('staffUncoveredShort', {
label: subject.label, label: subject.label,
min: subject.gradeMin, min: subject.gradeMin,
max: subject.gradeMax, max: subject.gradeMax,
n: subject.teachersShort,
}), }),
}), }),
); );
+36 -5
View File
@@ -18,6 +18,14 @@ public enum StaffingError
NoOpening, NoOpening,
} }
/// <summary>
/// A subject the school cannot actually teach, and how many more teachers would cover it.
/// </summary>
public sealed record UncoveredSubject(SubjectDef Subject, int Assigned, int TeachersShort)
{
public string DefName => Subject.DefName;
}
public sealed record StaffingOutcome( public sealed record StaffingOutcome(
StaffingError Error, StaffingError Error,
Roster Roster, Roster Roster,
@@ -130,12 +138,32 @@ public static class Staffing
return Round(total); return Round(total);
} }
/// <summary>
/// How many people a subject needs at the weekly cap. Primary school on a vanilla map is
/// eighty hours: one person cannot carry it, and the uncovered row has to say so.
/// </summary>
public static int TeachersToCover(float hours, float maxWeeklyHours)
{
if (hours <= 0f)
{
return 0;
}
if (maxWeeklyHours <= 0f)
{
return 1;
}
return (int)Math.Ceiling(hours / maxWeeklyHours);
}
/// <summary> /// <summary>
/// Subjects the school cannot actually teach: nobody is assigned them, or the people who are /// Subjects the school cannot actually teach: nobody is assigned them, or the people who are
/// cannot between them carry the curriculum hours. Both read the same way to a player — the /// cannot between them carry the curriculum hours. Both read the same way to a player — the
/// lessons will not happen — so both belong in one list. /// lessons will not happen — so both belong in one list. <see cref="UncoveredSubject.TeachersShort"/>
/// is how many more people it still needs.
/// </summary> /// </summary>
public static IReadOnlyList<SubjectDef> Uncovered(DefCatalog catalog, Roster roster) public static IReadOnlyList<UncoveredSubject> Uncovered(DefCatalog catalog, Roster roster)
{ {
var years = new HashSet<int>(); var years = new HashSet<int>();
foreach (var schoolClass in roster.Classes) foreach (var schoolClass in roster.Classes)
@@ -158,7 +186,7 @@ public static class Staffing
} }
var cap = catalog.StaffingRules?.MaxWeeklyHours ?? 0f; var cap = catalog.StaffingRules?.MaxWeeklyHours ?? 0f;
var uncovered = new List<SubjectDef>(); var uncovered = new List<UncoveredSubject>();
foreach (var subject in catalog.Subjects.Values) foreach (var subject in catalog.Subjects.Values)
{ {
if (subject.Abstract || !TouchesYears(subject, years)) if (subject.Abstract || !TouchesYears(subject, years))
@@ -167,9 +195,12 @@ public static class Staffing
} }
var assigned = teachers.GetValueOrDefault(subject.DefName); var assigned = teachers.GetValueOrDefault(subject.DefName);
if (assigned == 0 || (cap > 0f && SubjectHours(catalog, roster, subject.DefName) > assigned * cap)) var hours = SubjectHours(catalog, roster, subject.DefName);
var needed = TeachersToCover(hours, cap);
var shortfall = Math.Max(0, needed - assigned);
if (shortfall > 0)
{ {
uncovered.Add(subject); uncovered.Add(new UncoveredSubject(subject, assigned, shortfall));
} }
} }
+11 -8
View File
@@ -196,7 +196,8 @@ internal sealed record UncoveredSubjectResponse(
string Label, string Label,
int GradeMin, int GradeMin,
int GradeMax, int GradeMax,
int HoursPerWeek); int HoursPerWeek,
int TeachersShort);
internal sealed record ApplicantResponse( internal sealed record ApplicantResponse(
string Id, string Id,
@@ -241,12 +242,13 @@ internal static class StaffingMapper
var uncovered = catalog is null var uncovered = catalog is null
? Array.Empty<UncoveredSubjectResponse>() ? Array.Empty<UncoveredSubjectResponse>()
: Staffing.Uncovered(catalog, roster) : Staffing.Uncovered(catalog, roster)
.Select(subject => new UncoveredSubjectResponse( .Select(row => new UncoveredSubjectResponse(
subject.DefName, row.Subject.DefName,
catalog.Label(locale, subject), catalog.Label(locale, row.Subject),
subject.Grades.Min, row.Subject.Grades.Min,
subject.Grades.Max, row.Subject.Grades.Max,
subject.HoursPerWeek)) row.Subject.HoursPerWeek,
row.TeachersShort))
.ToArray(); .ToArray();
var applicants = pool.Applicants var applicants = pool.Applicants
@@ -286,7 +288,8 @@ internal static class StaffingMapper
catalog.Label(locale, def), catalog.Label(locale, def),
def.Grades.Min, def.Grades.Min,
def.Grades.Max, def.Grades.Max,
def.HoursPerWeek)) def.HoursPerWeek,
0))
.ToArray(); .ToArray();
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff, positions, subjects); return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff, positions, subjects);
+3
View File
@@ -70,6 +70,9 @@ if (app.Configuration.GetValue("HSchool:AllowSaveReload", false))
return Results.NoContent(); return Results.NoContent();
}) })
.WithName("ReloadSchoolsFromDisk"); .WithName("ReloadSchoolsFromDisk");
app.MapGet("/api/dev/saves-directory", (SchoolStore store) => Results.Json(new { path = store.DirectoryPath }))
.WithName("GetSavesDirectory");
} }
// The realtime channel: one binary frame per protocol message, see docs/protocol.md. // The realtime channel: one binary frame per protocol message, see docs/protocol.md.
@@ -0,0 +1,100 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class GoldenSaveTests(AppHostFixture fixture)
{
[Fact]
public async Task CurrentFormatSave_LoadsTheSamePeopleAndTime()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
await InstallAsync(client, "current");
var state = await SchoolApiTests.GetSchoolsAsync(client);
var school = Assert.Single(state.Schools);
Assert.Equal(1, school.Id);
Assert.Equal("Золотая", school.Name);
Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime);
Assert.False(school.Running);
var people = await PeopleAsync(client, school.Id);
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
Assert.Contains(people, person => person.Roles.Contains("student"));
}
[Fact]
public async Task SaveWithoutNativeLanguage_LoadsWithoutReshuffling()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
await InstallAsync(client, "legacy-no-native");
var state = await SchoolApiTests.GetSchoolsAsync(client);
var school = Assert.Single(state.Schools);
Assert.Equal("Золотая", school.Name);
var people = await PeopleAsync(client, school.Id);
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
}
private async Task InstallAsync(HttpClient client, string folder)
{
var directory = await SavesDirectoryAsync(client);
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
foreach (var file in Directory.EnumerateFiles(source))
{
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
}
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
}
private static async Task<string> SavesDirectoryAsync(HttpClient client)
{
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
"/api/dev/saves-directory",
TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
return payload.Path;
}
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
{
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{schoolId}/people?pageSize=100",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
return page.People
.OrderBy(row => row.Id, StringComparer.Ordinal)
.ToArray();
}
private static string[] ExpectedNames()
{
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "golden", "current", "1.people.json"));
using var document = JsonDocument.Parse(json);
return document.RootElement.GetProperty("people")
.EnumerateArray()
.Select(person =>
{
var name = person.GetProperty("name");
var surname = name.GetProperty("surname").GetString() ?? "";
var given = name.GetProperty("given").GetString() ?? "";
var patronymic = name.GetProperty("patronymic").GetString() ?? "";
return string.Join(' ', new[] { surname, given, patronymic }.Where(part => part.Length > 0));
})
.OrderBy(full => full, StringComparer.Ordinal)
.ToArray();
}
private sealed record SavesDirectoryResponse(string Path);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
}
@@ -26,4 +26,10 @@
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="golden\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>
@@ -32,7 +32,8 @@ public class StaffingApiTests(AppHostFixture fixture)
Assert.Equal(32, staffing.Applicants.Count); Assert.Equal(32, staffing.Applicants.Count);
Assert.Empty(staffing.Staff); Assert.Empty(staffing.Staff);
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "Mathematics"); Assert.Contains(staffing.Uncovered, subject => subject.DefName == "Mathematics");
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "PrimarySchool"); var primary = Assert.Single(staffing.Uncovered, subject => subject.DefName == "PrimarySchool");
Assert.Equal(3, primary.TeachersShort);
Assert.NotEmpty(staffing.Positions); Assert.NotEmpty(staffing.Positions);
Assert.Contains(staffing.Positions, position => position.DefName == "Teacher"); Assert.Contains(staffing.Positions, position => position.DefName == "Teacher");
Assert.Contains(staffing.Subjects, subject => subject.DefName == "Mathematics"); Assert.Contains(staffing.Subjects, subject => subject.DefName == "Mathematics");
@@ -331,7 +332,7 @@ public class StaffingApiTests(AppHostFixture fixture)
IReadOnlyList<DefLabelResponse> Positions, IReadOnlyList<DefLabelResponse> Positions,
IReadOnlyList<UncoveredSubjectResponse> Subjects); IReadOnlyList<UncoveredSubjectResponse> Subjects);
private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek); private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek, int TeachersShort);
private sealed record ApplicantResponse( private sealed record ApplicantResponse(
string Id, string Id,
@@ -0,0 +1,14 @@
# Golden saves
`current/` is a school file of today's format. `legacy-no-native/` is the same school without
`nativeLanguage`, so a missing field still has to mean "first language of the name set" rather
than a new roll.
These files live forever: a new save field must load `current/` and must not reshuffle
`legacy-no-native/`. A real format change adds a new folder; it does not rewrite the old one.
Regenerate after an intentional roster change (same commit):
```
WRITE_GOLDEN=1 dotnet test tests/HSchool.People.Tests/HSchool.People.Tests.csproj --filter Dump_HostSaveFixtures
```
@@ -0,0 +1,63 @@
{
"format": 2,
"id": 1,
"name": "Золотая",
"gameTime": "2012-03-31T06:00:00Z",
"running": false,
"speedIndex": 1,
"modIds": [
"core"
],
"map": {
"territory": {
"id": "yard",
"def": "SchoolYard"
},
"buildings": [
{
"id": "main",
"def": "MainBuilding"
}
],
"floors": [
{
"id": "floor-1",
"def": "StandardFloor",
"building": "main",
"label": "1"
}
],
"rooms": [
{
"id": "classroom-00",
"def": "Classroom",
"building": "main",
"floor": "floor-1",
"label": "101",
"seats": 16,
"slots": []
},
{
"id": "classroom-01",
"def": "Classroom",
"building": "main",
"floor": "floor-1",
"label": "102",
"seats": 16,
"slots": []
}
],
"links": [
{
"a": "yard",
"b": "classroom-00"
},
{
"a": "yard",
"b": "classroom-01"
}
]
},
"nameSetId": "Slavic",
"nativeLanguage": "RussianLanguage"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
{
"format": 2,
"id": 1,
"name": "Золотая",
"gameTime": "2012-03-31T06:00:00Z",
"running": false,
"speedIndex": 1,
"modIds": [
"core"
],
"map": {
"territory": {
"id": "yard",
"def": "SchoolYard"
},
"buildings": [
{
"id": "main",
"def": "MainBuilding"
}
],
"floors": [
{
"id": "floor-1",
"def": "StandardFloor",
"building": "main",
"label": "1"
}
],
"rooms": [
{
"id": "classroom-00",
"def": "Classroom",
"building": "main",
"floor": "floor-1",
"label": "101",
"seats": 16,
"slots": []
},
{
"id": "classroom-01",
"def": "Classroom",
"building": "main",
"floor": "floor-1",
"label": "102",
"seats": 16,
"slots": []
}
],
"links": [
{
"a": "yard",
"b": "classroom-00"
},
{
"a": "yard",
"b": "classroom-01"
}
]
},
"nameSetId": "Slavic"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
namespace HSchool.People.Tests;
public class GoldenRosterTests
{
private const string Native = "RussianLanguage";
[Fact]
public void GoldenRoster_MatchesFixture()
{
var roster = Fixtures.Generate(Fixtures.Classrooms(4));
var actual = RosterFingerprint.Format(roster, Fixtures.SchoolSeed, "Slavic", Native);
var relative = Path.Combine("golden", "roster.txt");
var output = Path.Combine(AppContext.BaseDirectory, relative);
var source = Path.Combine(Fixtures.RepoRoot(), "tests", "HSchool.People.Tests", relative);
if (string.Equals(Environment.GetEnvironmentVariable("WRITE_GOLDEN"), "1", StringComparison.Ordinal))
{
Directory.CreateDirectory(Path.GetDirectoryName(source)!);
File.WriteAllText(source, actual);
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
File.WriteAllText(output, actual);
}
Assert.True(File.Exists(output), $"Golden roster is missing at {output}.");
RosterFingerprint.AssertEqual(File.ReadAllText(output), actual);
}
}
@@ -0,0 +1,103 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using HSchool.Content;
namespace HSchool.People.Tests;
/// <summary>
/// Writes the host golden save files. Not a correctness test — run with WRITE_GOLDEN=1 after an
/// intentional generation change, then commit the AppHost fixtures in the same change.
/// </summary>
public class GoldenSaveDump
{
[Fact]
public void Dump_HostSaveFixtures_WhenAsked()
{
if (!string.Equals(Environment.GetEnvironmentVariable("WRITE_GOLDEN"), "1", StringComparison.Ordinal))
{
return;
}
var catalog = Fixtures.Catalog();
var map = TwoClassrooms();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
var people = RosterJson.Serialize(RosterDocument.From(1, roster, pool));
var mapNode = JsonNode.Parse(JsonSerializer.Serialize(map, Jsonc.SerializerOptions));
var current = SchoolJson(mapNode, includeNative: true);
var legacy = SchoolJson(mapNode, includeNative: false);
var root = Path.Combine(Fixtures.RepoRoot(), "tests", "HSchool.AppHost.Tests", "golden");
Write(Path.Combine(root, "current", "1.json"), current);
Write(Path.Combine(root, "current", "1.people.json"), people);
Write(Path.Combine(root, "legacy-no-native", "1.json"), legacy);
Write(Path.Combine(root, "legacy-no-native", "1.people.json"), people);
}
internal static MapLayout TwoClassrooms() =>
new()
{
Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" },
Buildings = [new BuildingNode { Id = "main", Def = "MainBuilding" }],
Floors = [new FloorNode { Id = "floor-1", Def = "StandardFloor", Building = "main", Label = "1" }],
Rooms =
[
new RoomNode
{
Id = "classroom-00",
Def = "Classroom",
Building = "main",
Floor = "floor-1",
Label = "101",
Seats = 16,
},
new RoomNode
{
Id = "classroom-01",
Def = "Classroom",
Building = "main",
Floor = "floor-1",
Label = "102",
Seats = 16,
},
],
Links =
[
new MapLink { A = "yard", B = "classroom-00" },
new MapLink { A = "yard", B = "classroom-01" },
],
};
private static string SchoolJson(JsonNode? map, bool includeNative)
{
var root = new JsonObject
{
["format"] = 2,
["id"] = 1,
["name"] = "Золотая",
["gameTime"] = "2012-03-31T06:00:00Z",
["running"] = false,
["speedIndex"] = 1,
["modIds"] = new JsonArray("core"),
["map"] = map?.DeepClone(),
["nameSetId"] = "Slavic",
};
if (includeNative)
{
root["nativeLanguage"] = "RussianLanguage";
}
return root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
});
}
private static void Write(string path, string contents)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, contents);
}
}
@@ -26,6 +26,9 @@
<Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link> <Link>vanilla\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="golden\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,78 @@
namespace HSchool.People.Tests;
/// <summary>
/// One line per class and person, sorted, so a generator change shows up as the first
/// mismatched line instead of "the snapshots differ".
/// </summary>
internal static class RosterFingerprint
{
public static string Format(Roster roster, int seed, string nameSet, string native)
{
var lines = new List<string>
{
$"# seed={seed} nameset={nameSet} native={native} people={roster.People.Count} classes={roster.Classes.Count}",
};
foreach (var schoolClass in roster.Classes.OrderBy(row => row.Id, StringComparer.Ordinal))
{
var pupils = string.Join(',', schoolClass.PupilIds.Order(StringComparer.Ordinal));
lines.Add(
$"class {schoolClass.Id} year={schoolClass.Year} letter={schoolClass.Letter} room={schoolClass.RoomId} capacity={schoolClass.Capacity} pupils={pupils}");
}
foreach (var person in roster.People.OrderBy(row => row.Id, StringComparer.Ordinal))
{
lines.Add(PersonLine(person));
}
return string.Join('\n', lines) + '\n';
}
public static void AssertEqual(string expected, string actual)
{
var left = expected.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
var right = actual.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
var count = Math.Min(left.Length, right.Length);
for (var i = 0; i < count; i++)
{
if (!string.Equals(left[i], right[i], StringComparison.Ordinal))
{
Assert.Fail($"first mismatch at line {i + 1}\nexpected: {left[i]}\nactual: {right[i]}");
}
}
if (left.Length != right.Length)
{
Assert.Fail($"line count {left.Length} vs {right.Length}");
}
}
private static string PersonLine(Person person)
{
var roles = string.Join(',', Roles(person));
var traits = string.Join(',', person.Traits.Order(StringComparer.Ordinal));
var skills = string.Join(
',',
person.Skills.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
return
$"person {person.Id} {person.Name.Full} female={person.Female} roles={roles} class={person.ClassId ?? "-"} born={person.BirthDate:yyyy-MM-dd} traits={traits} skills={skills}";
}
private static IEnumerable<string> Roles(Person person)
{
if (person.IsStudent)
{
yield return "student";
}
if (person.IsStaff)
{
yield return "staff";
}
if (person.IsParent)
{
yield return "parent";
}
}
}
@@ -232,6 +232,28 @@ public class StaffingTests
Assert.Contains(Staffing.Uncovered(catalog, assigned.Roster), subject => subject.DefName == "PrimarySchool"); Assert.Contains(Staffing.Uncovered(catalog, assigned.Roster), subject => subject.DefName == "PrimarySchool");
} }
[Fact]
public void PrimarySchool_OnVanillaMap_NeedsThreeTeachers_AndASecondDoesNotCloseIt()
{
var (catalog, map, roster, pool) = Fresh();
var rules = catalog.StaffingRules!;
Assert.Equal(80f, Staffing.SubjectHours(catalog, roster, "PrimarySchool"));
Assert.Equal(3, Staffing.TeachersToCover(80f, rules.MaxWeeklyHours));
var empty = Staffing.Uncovered(catalog, roster).Single(subject => subject.DefName == "PrimarySchool");
Assert.Equal(3, empty.TeachersShort);
var outcome = new StaffingOutcome(StaffingError.None, roster, pool, 0, 0, 0, 0);
outcome = HireAndAssign(catalog, map, outcome, "PrimarySchool");
Assert.Equal(2, Staffing.Uncovered(catalog, outcome.Roster).Single(subject => subject.DefName == "PrimarySchool").TeachersShort);
outcome = HireAndAssign(catalog, map, outcome, "PrimarySchool");
Assert.Equal(1, Staffing.Uncovered(catalog, outcome.Roster).Single(subject => subject.DefName == "PrimarySchool").TeachersShort);
outcome = HireAndAssign(catalog, map, outcome, "PrimarySchool");
Assert.DoesNotContain(Staffing.Uncovered(catalog, outcome.Roster), subject => subject.DefName == "PrimarySchool");
}
[Fact] [Fact]
public void PeopleJson_RoundTripsHiredStaffAndSubjects() public void PeopleJson_RoundTripsHiredStaffAndSubjects()
{ {
@@ -274,6 +296,20 @@ public class StaffingTests
return (catalog, map, roster, pool); return (catalog, map, roster, pool);
} }
private static StaffingOutcome HireAndAssign(
DefCatalog catalog,
MapLayout map,
StaffingOutcome current,
string subject)
{
var applicant = current.Pool.Applicants[0];
var hired = Staffing.Hire(catalog, map, current.Roster, current.Pool, applicant.Person.Id, Staffing.TeacherPosition, 500_000f);
Assert.Equal(StaffingError.None, hired.Error);
var assigned = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, applicant.Person.Id, subject, 500_000f);
Assert.Equal(StaffingError.None, assigned.Error);
return assigned;
}
private static Applicant WithAsk(Applicant applicant, float hourlyAsk) => private static Applicant WithAsk(Applicant applicant, float hourlyAsk) =>
applicant with { HourlyWageAsk = hourlyAsk }; applicant with { HourlyWageAsk = hourlyAsk };
} }
@@ -0,0 +1,14 @@
# Golden roster
`roster.txt` is the fingerprint of `RosterGenerator` for a fixed seed, four classrooms, Slavic
names and Russian as the native language.
Update it **in the same commit that changes generation**, and say in the commit message what
changed (a new skill, a related-language roll, a different birth window). A red golden test is
the question "did you mean to?", not an order to revert.
To rewrite the file after an intentional change:
```
WRITE_GOLDEN=1 dotnet test tests/HSchool.People.Tests/HSchool.People.Tests.csproj --filter GoldenRoster_MatchesFixture
```
@@ -0,0 +1,135 @@
# seed=20260818 nameset=Slavic native=RussianLanguage people=130 classes=4
class class-classroom-00 year=1 letter=А room=classroom-00 capacity=16 pupils=f1.c1,f1.c2,f13.c0,f18.c0,f19.c0,f24.c1,f26.c1,f27.c1,f27.c2,f28.c2,f32.c1,f33.c0,f34.c0,f6.c0,f6.c2,f8.c0
class class-classroom-01 year=2 letter=А room=classroom-01 capacity=16 pupils=f0.c1,f12.c0,f12.c1,f12.c2,f13.c1,f13.c2,f14.c2,f20.c0,f23.c1,f26.c0,f27.c0,f29.c0,f31.c0,f4.c0,f7.c0,f9.c0
class class-classroom-02 year=3 letter=А room=classroom-02 capacity=16 pupils=f10.c0,f14.c0,f16.c0,f17.c1,f2.c0,f21.c0,f22.c1,f23.c0,f25.c0,f28.c0,f3.c0,f3.c1,f32.c0,f32.c2,f5.c0,f6.c1
class class-classroom-03 year=4 letter=А room=classroom-03 capacity=16 pupils=f0.c0,f1.c0,f11.c0,f14.c1,f15.c0,f17.c0,f17.c2,f20.c1,f22.c0,f24.c0,f28.c1,f30.c0,f30.c1,f8.c1,f9.c1,f9.c2
person f0.c0 Андреев Сергей Валериевич female=False roles=student class=class-classroom-03 born=2000-10-23 traits=Quiet skills=Agility=23,BelarusianLanguage=6,Biology=57,Communication=29,Endurance=51,Literature=20,Mathematics=14,PhysicalEducation=77,RussianLanguage=32,Strength=43,UkrainianLanguage=24
person f0.c1 Андреев Александр Валериевич female=False roles=student class=class-classroom-01 born=2002-11-09 traits=Curious,Quiet skills=Agility=47,BelarusianLanguage=14,Biology=56,Communication=42,Endurance=51,Literature=40,Mathematics=35,PhysicalEducation=41,RussianLanguage=45,Strength=45,UkrainianLanguage=40
person f0.p0 Андреев Валерий Николаевич female=False roles=parent class=- born=1971-06-03 traits=Diligent,Quiet skills=Agility=43,Communication=58,Endurance=61,English=62,RussianLanguage=46,Strength=63,UkrainianLanguage=37
person f0.p1 Андреева Марина Артемовна female=True roles=parent class=- born=1971-09-30 traits= skills=Agility=30,Communication=35,Endurance=46,English=58,Informatics=59,Librarianship=63,Medicine=73,PhysicalEducation=45,RussianLanguage=68,Strength=52
person f1.c0 Александрова Людмила Борисовна female=True roles=student class=class-classroom-03 born=2001-01-26 traits= skills=Agility=72,Biology=49,Communication=37,Endurance=39,Literature=69,Mathematics=66,PhysicalEducation=65,RussianLanguage=41,Strength=45,UkrainianLanguage=27
person f1.c1 Александров Евгений Борисович female=False roles=student class=class-classroom-00 born=2004-04-01 traits=Diligent,Kind skills=Agility=59,BelarusianLanguage=40,Biology=62,Communication=16,Endurance=56,Literature=83,Mathematics=36,PhysicalEducation=45,RussianLanguage=46,Strength=45
person f1.c2 Александров Юрий Борисович female=False roles=student class=class-classroom-00 born=2003-10-28 traits= skills=Agility=44,BelarusianLanguage=27,Biology=73,Communication=24,Endurance=67,Literature=70,Mathematics=24,PhysicalEducation=54,RussianLanguage=15,Strength=45
person f1.p0 Александров Борис Ильич female=False roles=parent class=- born=1968-10-25 traits= skills=Agility=45,Communication=74,Endurance=34,English=63,Librarianship=51,Mathematics=66,RussianLanguage=59,Strength=68,UkrainianLanguage=24
person f1.p1 Александрова Елизавета Романовна female=True roles=parent class=- born=1972-01-25 traits=Diligent skills=Agility=61,BelarusianLanguage=29,Communication=35,Endurance=39,English=52,Mathematics=72,RussianLanguage=61,Strength=45,UkrainianLanguage=21
person f10.c0 Захаров Пётр Виталиевич female=False roles=student class=class-classroom-02 born=2001-11-08 traits= skills=Agility=64,BelarusianLanguage=27,Biology=49,Communication=37,Endurance=47,Literature=51,Mathematics=30,PhysicalEducation=42,RussianLanguage=35,Strength=37,UkrainianLanguage=24
person f10.p0 Захаров Виталий Борисович female=False roles=parent class=- born=1976-09-07 traits=HotTempered,Leader skills=Agility=44,Chemistry=93,Communication=66,Endurance=82,English=31,RussianLanguage=60,Strength=40
person f10.p1 Захарова Любовь Валериевна female=True roles=parent class=- born=1978-11-03 traits=AbsentMinded,Neat skills=Administration=66,Agility=36,BelarusianLanguage=38,Communication=64,Endurance=79,Informatics=51,Medicine=64,RussianLanguage=63,Strength=80,UkrainianLanguage=40
person f11.c0 Андреева Дарья Ивановна female=True roles=student class=class-classroom-03 born=2001-05-20 traits= skills=Agility=60,BelarusianLanguage=22,Biology=60,Communication=36,Endurance=54,Literature=42,Mathematics=54,PhysicalEducation=48,RussianLanguage=35,Strength=45,UkrainianLanguage=22
person f11.p0 Андреев Иван Алексеевич female=False roles=parent class=- born=1976-06-01 traits= skills=Agility=38,BelarusianLanguage=27,Communication=52,Endurance=82,RussianLanguage=72,Strength=30
person f11.p1 Андреева София Александровна female=True roles=parent class=- born=1978-04-24 traits= skills=Agility=39,BelarusianLanguage=18,Communication=41,Endurance=50,English=55,Geography=59,Informatics=47,RussianLanguage=62,Strength=45
person f12.c0 Кузьмин Олег Владиславович female=False roles=student class=class-classroom-01 born=2003-03-20 traits=AbsentMinded,Bully skills=Agility=51,BelarusianLanguage=30,Biology=64,Communication=54,Endurance=40,Literature=54,Mathematics=32,PhysicalEducation=54,RussianLanguage=40,Strength=70,UkrainianLanguage=18
person f12.c1 Кузьмин Егор Владиславович female=False roles=student class=class-classroom-01 born=2003-05-31 traits=HotTempered,Kind skills=Agility=19,BelarusianLanguage=10,Biology=45,Communication=43,Endurance=42,Literature=60,Mathematics=24,PhysicalEducation=50,RussianLanguage=38,Strength=45
person f12.c2 Кузьмина Дарья Владиславовна female=True roles=student class=class-classroom-01 born=2002-10-29 traits=Curious skills=Agility=43,Biology=42,Communication=37,Endurance=34,Literature=44,Mathematics=25,PhysicalEducation=57,RussianLanguage=38,Strength=45,UkrainianLanguage=18
person f12.p0 Кузьмин Владислав Александрович female=False roles=parent class=- born=1975-01-17 traits= skills=Agility=25,BelarusianLanguage=27,Communication=68,Endurance=35,Literature=53,Mathematics=41,RussianLanguage=43,Strength=65,UkrainianLanguage=14
person f12.p1 Кузьмина Ольга Кирилловна female=True roles=parent class=- born=1979-10-15 traits=Kind,Lazy skills=Administration=58,Agility=54,BelarusianLanguage=15,Communication=65,Endurance=40,English=50,Physics=34,RussianLanguage=52,Strength=63,UkrainianLanguage=16
person f13.c0 Яковлев Константин Владиславович female=False roles=student class=class-classroom-00 born=2004-06-16 traits=HotTempered skills=Agility=67,Biology=45,Communication=21,Endurance=49,Literature=60,Mathematics=14,PhysicalEducation=63,RussianLanguage=30,Strength=39,UkrainianLanguage=18
person f13.c1 Яковлев Юрий Владиславович female=False roles=student class=class-classroom-01 born=2003-07-23 traits=Kind skills=Agility=77,Biology=69,Communication=26,Endurance=55,Literature=54,Mathematics=27,PhysicalEducation=41,RussianLanguage=48,Strength=45
person f13.c2 Яковлева Алина Владиславовна female=True roles=student class=class-classroom-01 born=2003-02-01 traits=Lazy,Neat skills=Agility=42,BelarusianLanguage=22,Biology=66,Communication=43,Endurance=59,Literature=42,Mathematics=38,PhysicalEducation=72,RussianLanguage=40,Strength=54
person f13.p0 Яковлев Владислав Андреевич female=False roles=parent class=- born=1974-09-23 traits=AbsentMinded,Kind skills=Administration=61,Agility=25,Communication=42,Endurance=35,Geography=31,German=44,History=68,RussianLanguage=44,Strength=52
person f14.c0 Александров Степан Денисович female=False roles=student class=class-classroom-02 born=2002-01-03 traits=Bully,Lazy skills=Agility=26,BelarusianLanguage=20,Biology=42,Communication=45,Endurance=58,Literature=54,Mathematics=5,PhysicalEducation=51,RussianLanguage=29,Strength=49,UkrainianLanguage=17
person f14.c1 Александрова Вероника Денисовна female=True roles=student class=class-classroom-03 born=2001-08-11 traits=AbsentMinded skills=Agility=52,BelarusianLanguage=22,Biology=59,Communication=38,Endurance=40,Literature=52,Mathematics=9,PhysicalEducation=62,RussianLanguage=53,Strength=43
person f14.c2 Александров Константин Денисович female=False roles=student class=class-classroom-01 born=2003-08-21 traits= skills=Agility=45,BelarusianLanguage=22,Biology=35,Communication=16,Endurance=36,Literature=62,Mathematics=47,PhysicalEducation=65,RussianLanguage=26,Strength=51
person f14.p1 Александрова Виктория Викторовна female=True roles=parent class=- born=1973-07-23 traits= skills=Agility=45,BelarusianLanguage=23,Communication=72,Cooking=33,Endurance=64,English=36,PhysicalEducation=61,RussianLanguage=55,Strength=42
person f15.c0 Воробьёв Игорь Сергеевич female=False roles=student class=class-classroom-03 born=2000-09-20 traits=Diligent skills=Agility=40,BelarusianLanguage=12,Biology=42,Communication=51,Endurance=51,Literature=37,Mathematics=16,PhysicalEducation=31,RussianLanguage=11,Strength=51,UkrainianLanguage=11
person f15.p0 Воробьёв Сергей Никитич female=False roles=parent class=- born=1974-10-22 traits=Neat,Quiet skills=Agility=45,BelarusianLanguage=28,Communication=54,Endurance=45,English=62,RussianLanguage=70,Strength=33,UkrainianLanguage=16
person f15.p1 Воробьёва Вера Виталиевна female=True roles=parent class=- born=1972-09-07 traits=Curious skills=Agility=60,Communication=60,Endurance=52,Geography=42,History=40,RussianLanguage=40,Strength=64,UkrainianLanguage=25
person f16.c0 Фролова Елизавета Игоревна female=True roles=student class=class-classroom-02 born=2002-06-29 traits=Lazy,Quiet skills=Agility=65,BelarusianLanguage=14,Biology=19,Communication=30,Endurance=34,Literature=33,Mathematics=27,PhysicalEducation=42,RussianLanguage=17,Strength=67
person f16.p0 Фролов Игорь Виталиевич female=False roles=parent class=- born=1968-07-25 traits= skills=Agility=57,BelarusianLanguage=37,Communication=58,Endurance=63,English=52,RussianLanguage=52,Strength=49,UkrainianLanguage=20
person f16.p1 Фролова Татьяна Олеговна female=True roles=parent class=- born=1970-06-17 traits=Curious,Diligent skills=Agility=80,BelarusianLanguage=5,Communication=63,Endurance=71,RussianLanguage=71,Strength=52
person f17.c0 Иванов Андрей Александрович female=False roles=student class=class-classroom-03 born=2001-06-27 traits= skills=Agility=44,BelarusianLanguage=13,Biology=29,Communication=39,Endurance=60,Literature=49,Mathematics=34,PhysicalEducation=60,RussianLanguage=24,Strength=55
person f17.c1 Иванова Ксения Александровна female=True roles=student class=class-classroom-02 born=2001-09-10 traits= skills=Agility=45,BelarusianLanguage=21,Biology=25,Communication=28,Endurance=64,Literature=66,Mathematics=37,PhysicalEducation=59,RussianLanguage=58,Strength=45
person f17.c2 Иванова Полина Александровна female=True roles=student class=class-classroom-03 born=2001-05-21 traits=HotTempered,Kind skills=Agility=36,Biology=53,Communication=45,Endurance=45,Literature=43,Mathematics=29,PhysicalEducation=51,RussianLanguage=42,Strength=45,UkrainianLanguage=23
person f17.p0 Иванов Александр Владимирович female=False roles=parent class=- born=1967-12-24 traits=Diligent,HotTempered skills=Agility=47,Communication=74,Cooking=58,Endurance=52,English=20,RussianLanguage=61,Strength=55
person f17.p1 Иванова Александра Ивановна female=True roles=parent class=- born=1967-04-25 traits= skills=Agility=40,BelarusianLanguage=7,Communication=69,Endurance=63,Geography=64,Literature=72,RussianLanguage=73,Strength=40
person f18.c0 Лебедев Александр Валериевич female=False roles=student class=class-classroom-00 born=2004-02-16 traits= skills=Agility=49,BelarusianLanguage=28,Biology=36,Communication=39,Endurance=28,Literature=38,Mathematics=32,PhysicalEducation=36,RussianLanguage=37,Strength=21
person f18.p0 Лебедев Валерий Романович female=False roles=parent class=- born=1985-03-29 traits=Neat,Quiet skills=Agility=25,Communication=59,Endurance=35,English=60,Mathematics=59,Medicine=44,Pedagogy=62,Physics=45,RussianLanguage=67,Strength=33,UkrainianLanguage=19
person f18.p1 Лебедева Юлия Михаиловна female=True roles=parent class=- born=1981-01-26 traits=Leader skills=Agility=44,Communication=73,Endurance=86,Geography=30,Medicine=45,Pedagogy=43,RussianLanguage=57,Strength=41,UkrainianLanguage=36
person f19.c0 Воробьёв Григорий Михаилович female=False roles=student class=class-classroom-00 born=2004-08-29 traits=Quiet skills=Agility=62,Biology=45,Communication=36,Endurance=50,Literature=42,Mathematics=12,PhysicalEducation=56,RussianLanguage=54,Strength=37
person f19.p0 Воробьёв Михаил Егорович female=False roles=parent class=- born=1978-11-03 traits=Lazy skills=Agility=40,BelarusianLanguage=34,Communication=56,Cooking=54,Endurance=46,History=51,RussianLanguage=61,Strength=40
person f19.p1 Воробьёва Вера Алексеевна female=True roles=parent class=- born=1982-08-29 traits=HotTempered skills=Administration=50,Agility=45,BelarusianLanguage=20,Communication=67,Cooking=21,Endurance=57,Geography=41,PhysicalEducation=46,RussianLanguage=36,Strength=40,UkrainianLanguage=25
person f2.c0 Алексеев Григорий Константинович female=False roles=student class=class-classroom-02 born=2002-07-24 traits=AbsentMinded,Curious skills=Agility=35,BelarusianLanguage=27,Biology=50,Communication=30,Endurance=68,Literature=50,Mathematics=8,PhysicalEducation=72,RussianLanguage=9,Strength=45,UkrainianLanguage=17
person f2.p0 Алексеев Константин Романович female=False roles=parent class=- born=1974-12-11 traits=Neat skills=Administration=52,Agility=36,Communication=60,Cooking=55,Endurance=49,English=30,Geography=52,Mathematics=65,RussianLanguage=64,Strength=47,UkrainianLanguage=9
person f2.p1 Алексеева Ольга Романовна female=True roles=parent class=- born=1970-07-22 traits=HotTempered skills=Administration=45,Agility=45,BelarusianLanguage=22,Communication=82,Endurance=54,English=46,History=43,Librarianship=65,RussianLanguage=65,Strength=64
person f20.c0 Павлова Елена Игоревна female=True roles=student class=class-classroom-01 born=2002-11-09 traits= skills=Agility=48,Biology=38,Communication=37,Endurance=34,Literature=70,Mathematics=48,PhysicalEducation=51,RussianLanguage=26,Strength=40,UkrainianLanguage=21
person f20.c1 Павлова Валентина Игоревна female=True roles=student class=class-classroom-03 born=2001-01-24 traits= skills=Agility=46,BelarusianLanguage=19,Biology=50,Communication=32,Endurance=50,Literature=44,Mathematics=19,PhysicalEducation=52,RussianLanguage=39,Strength=38,UkrainianLanguage=27
person f20.p0 Павлов Игорь Никитич female=False roles=parent class=- born=1968-08-01 traits=HotTempered,Leader skills=Agility=72,Communication=57,Endurance=43,Informatics=54,Physics=43,RussianLanguage=49,Strength=43
person f20.p1 Павлова Людмила Дмитриевна female=True roles=parent class=- born=1970-01-17 traits= skills=Agility=60,BelarusianLanguage=28,Communication=48,Cooking=71,Endurance=56,RussianLanguage=63,Strength=46
person f21.c0 Ильин Андрей Константинович female=False roles=student class=class-classroom-02 born=2002-05-12 traits= skills=Agility=31,BelarusianLanguage=28,Biology=59,Communication=61,Endurance=48,Literature=47,Mathematics=34,PhysicalEducation=60,RussianLanguage=22,Strength=54
person f21.p0 Ильин Константин Никитич female=False roles=parent class=- born=1971-01-21 traits=Diligent,Quiet skills=Agility=45,BelarusianLanguage=30,Communication=53,Endurance=40,Medicine=40,Pedagogy=74,PhysicalEducation=48,Physics=57,RussianLanguage=45,Strength=44,UkrainianLanguage=19
person f21.p1 Ильина Яна Борисовна female=True roles=parent class=- born=1975-04-20 traits=Curious skills=Agility=35,BelarusianLanguage=13,Communication=62,Endurance=55,Informatics=60,Pedagogy=42,RussianLanguage=68,Strength=53,UkrainianLanguage=20
person f22.c0 Захаров Никита Виталиевич female=False roles=student class=class-classroom-03 born=2000-09-14 traits=Diligent,Quiet skills=Agility=48,Biology=62,Communication=33,Endurance=59,Literature=64,Mathematics=32,PhysicalEducation=49,RussianLanguage=70,Strength=69
person f22.c1 Захаров Денис Виталиевич female=False roles=student class=class-classroom-02 born=2001-09-11 traits=AbsentMinded skills=Agility=51,BelarusianLanguage=27,Biology=53,Communication=57,Endurance=53,Literature=74,Mathematics=18,PhysicalEducation=46,RussianLanguage=63,Strength=61
person f22.p0 Захаров Виталий Андреевич female=False roles=parent class=- born=1966-04-08 traits= skills=Agility=29,BelarusianLanguage=2,Communication=62,Cooking=40,Endurance=82,English=62,Librarianship=50,RussianLanguage=64,Strength=30
person f22.p1 Захарова Елизавета Егоровна female=True roles=parent class=- born=1967-09-05 traits=Diligent,HotTempered skills=Administration=51,Agility=51,BelarusianLanguage=6,Biology=40,Communication=56,Endurance=52,English=53,Librarianship=50,Literature=79,RussianLanguage=62,Strength=62
person f23.c0 Орловская Нина Константиновна female=True roles=student class=class-classroom-02 born=2001-11-01 traits=Neat skills=Agility=55,Biology=45,Communication=40,Endurance=55,Literature=16,Mathematics=38,PhysicalEducation=57,RussianLanguage=39,Strength=45,UkrainianLanguage=15
person f23.c1 Орловская Полина Константиновна female=True roles=student class=class-classroom-01 born=2003-03-30 traits=Leader,Neat skills=Agility=51,BelarusianLanguage=20,Biology=51,Communication=22,Endurance=42,Literature=39,Mathematics=14,PhysicalEducation=54,RussianLanguage=24,Strength=40
person f23.p0 Орловский Константин Михаилович female=False roles=parent class=- born=1965-04-02 traits=Diligent,HotTempered skills=Agility=45,Communication=55,Endurance=50,History=57,Librarianship=42,Mathematics=40,Pedagogy=35,RussianLanguage=46,Strength=50
person f23.p1 Орловская Яна Степановна female=True roles=parent class=- born=1965-10-22 traits= skills=Agility=42,Communication=52,Endurance=55,RussianLanguage=46,Strength=34,UkrainianLanguage=10
person f24.c0 Орлов Фёдор Алексеевич female=False roles=student class=class-classroom-03 born=2000-12-05 traits= skills=Agility=56,Biology=45,Communication=20,Endurance=35,Literature=67,Mathematics=23,PhysicalEducation=33,RussianLanguage=65,Strength=36
person f24.c1 Орлова Оксана Алексеевна female=True roles=student class=class-classroom-00 born=2004-03-01 traits=Neat skills=Agility=55,BelarusianLanguage=4,Biology=40,Communication=43,Endurance=43,Literature=39,Mathematics=17,PhysicalEducation=56,RussianLanguage=27,Strength=26,UkrainianLanguage=25
person f24.p0 Орлов Алексей Денисович female=False roles=parent class=- born=1977-04-17 traits= skills=Agility=45,BelarusianLanguage=21,Communication=51,Endurance=18,English=62,Geography=46,RussianLanguage=67,Strength=46
person f24.p1 Орлова Надежда Владиславовна female=True roles=parent class=- born=1977-12-04 traits=Quiet skills=Agility=65,Biology=64,Communication=70,Cooking=41,Endurance=45,Librarianship=49,RussianLanguage=71,Strength=65
person f25.c0 Петрова Александра Игоревна female=True roles=student class=class-classroom-02 born=2001-12-19 traits=AbsentMinded,Lazy skills=Agility=51,BelarusianLanguage=20,Biology=58,Communication=40,Endurance=54,Literature=65,Mathematics=50,PhysicalEducation=35,RussianLanguage=32,Strength=57
person f25.p1 Петрова Полина Юриевна female=True roles=parent class=- born=1965-12-11 traits=Kind,Neat skills=Agility=53,BelarusianLanguage=15,Communication=50,Endurance=59,RussianLanguage=54,Strength=45
person f26.c0 Яковлев Павел Михаилович female=False roles=student class=class-classroom-01 born=2002-12-29 traits=Curious skills=Agility=59,BelarusianLanguage=27,Biology=79,Communication=32,Endurance=57,Literature=35,Mathematics=10,PhysicalEducation=64,RussianLanguage=35,Strength=45
person f26.c1 Яковлев Вадим Михаилович female=False roles=student class=class-classroom-00 born=2004-06-28 traits=Neat skills=Agility=54,BelarusianLanguage=17,Biology=66,Communication=43,Endurance=52,Literature=40,Mathematics=18,PhysicalEducation=34,RussianLanguage=22,Strength=45
person f26.p0 Яковлев Михаил Степанович female=False roles=parent class=- born=1975-12-27 traits=HotTempered,Neat skills=Administration=44,Agility=44,Biology=52,Communication=59,Cooking=40,Endurance=66,English=56,Mathematics=55,RussianLanguage=52,Strength=53
person f26.p1 Яковлева Марина Кирилловна female=True roles=parent class=- born=1976-12-10 traits=HotTempered,Kind skills=Agility=37,BelarusianLanguage=21,Communication=47,Cooking=44,Endurance=31,Pedagogy=40,RussianLanguage=68,Strength=48,UkrainianLanguage=27
person f27.c0 Иванов Виктор Константинович female=False roles=student class=class-classroom-01 born=2002-12-27 traits=AbsentMinded,Curious skills=Agility=35,Biology=50,Communication=26,Endurance=35,Literature=60,Mathematics=1,PhysicalEducation=46,RussianLanguage=57,Strength=38
person f27.c1 Иванов Владислав Константинович female=False roles=student class=class-classroom-00 born=2004-07-04 traits= skills=Agility=50,BelarusianLanguage=24,Biology=61,Communication=24,Endurance=76,Literature=72,Mathematics=25,PhysicalEducation=46,RussianLanguage=34,Strength=45,UkrainianLanguage=17
person f27.c2 Иванов Александр Константинович female=False roles=student class=class-classroom-00 born=2004-05-25 traits=Kind,Neat skills=Agility=65,BelarusianLanguage=28,Biology=49,Communication=43,Endurance=68,Literature=42,Mathematics=46,PhysicalEducation=52,RussianLanguage=35,Strength=45,UkrainianLanguage=32
person f27.p0 Иванов Константин Иванович female=False roles=parent class=- born=1974-08-25 traits=Neat skills=Administration=49,Agility=44,BelarusianLanguage=34,Communication=77,Endurance=42,English=55,RussianLanguage=36,Strength=64,UkrainianLanguage=26
person f27.p1 Иванова Марина Константиновна female=True roles=parent class=- born=1969-11-29 traits=Quiet skills=Agility=46,BelarusianLanguage=21,Communication=61,Endurance=32,English=46,Librarianship=53,RussianLanguage=54,Strength=37
person f28.c0 Иванов Пётр Петрович female=False roles=student class=class-classroom-02 born=2002-08-02 traits=AbsentMinded,Neat skills=Agility=73,Biology=62,Communication=42,Endurance=43,Literature=59,Mathematics=48,PhysicalEducation=39,RussianLanguage=11,Strength=45
person f28.c1 Иванова Наталья Петровна female=True roles=student class=class-classroom-03 born=2000-12-07 traits=Bully,Lazy skills=Agility=64,BelarusianLanguage=30,Biology=46,Communication=27,Endurance=59,Literature=34,Mathematics=67,PhysicalEducation=69,RussianLanguage=51,Strength=45,UkrainianLanguage=23
person f28.c2 Иванов Максим Петрович female=False roles=student class=class-classroom-00 born=2004-04-05 traits=Lazy skills=Agility=67,BelarusianLanguage=13,Biology=59,Communication=26,Endurance=54,Literature=31,Mathematics=29,PhysicalEducation=63,RussianLanguage=55,Strength=51,UkrainianLanguage=25
person f28.p0 Иванов Пётр Олегович female=False roles=parent class=- born=1965-06-23 traits=AbsentMinded,Kind skills=Agility=75,BelarusianLanguage=26,Communication=60,Endurance=74,Informatics=60,Literature=57,Pedagogy=56,RussianLanguage=39,Strength=48
person f28.p1 Иванова Полина Алексеевна female=True roles=parent class=- born=1965-11-18 traits= skills=Agility=17,BelarusianLanguage=24,Chemistry=40,Communication=52,Endurance=35,Librarianship=59,Medicine=69,Physics=42,RussianLanguage=50,Strength=61,UkrainianLanguage=21
person f29.c0 Макарова Ксения Александровна female=True roles=student class=class-classroom-01 born=2002-11-08 traits=Bully skills=Agility=52,BelarusianLanguage=27,Biology=62,Communication=35,Endurance=53,Literature=71,Mathematics=48,PhysicalEducation=35,RussianLanguage=41,Strength=45
person f29.p0 Макаров Александр Николаевич female=False roles=parent class=- born=1981-11-08 traits=AbsentMinded skills=Administration=40,Agility=50,BelarusianLanguage=25,Chemistry=42,Communication=44,Endurance=55,Geography=56,Librarianship=66,RussianLanguage=69,Strength=40,UkrainianLanguage=19
person f29.p1 Макарова Мария Владиславовна female=True roles=parent class=- born=1977-10-31 traits=HotTempered skills=Agility=45,BelarusianLanguage=23,Chemistry=42,Communication=37,Endurance=70,Literature=20,RussianLanguage=54,Strength=44,UkrainianLanguage=25
person f3.c0 Макарова Вероника Петровна female=True roles=student class=class-classroom-02 born=2002-05-15 traits=Curious,Diligent skills=Agility=37,BelarusianLanguage=32,Biology=32,Communication=61,Endurance=71,Literature=63,Mathematics=27,PhysicalEducation=60,RussianLanguage=66,Strength=81
person f3.c1 Макаров Роман Петрович female=False roles=student class=class-classroom-02 born=2002-04-16 traits= skills=Agility=40,BelarusianLanguage=28,Biology=55,Communication=32,Endurance=34,Literature=51,Mathematics=41,PhysicalEducation=49,RussianLanguage=44,Strength=37
person f3.p0 Макаров Пётр Егорович female=False roles=parent class=- born=1977-02-28 traits=Diligent,Neat skills=Agility=67,Communication=28,Endurance=53,English=57,Librarianship=39,RussianLanguage=45,Strength=42,UkrainianLanguage=26
person f3.p1 Макарова Алина Антоновна female=True roles=parent class=- born=1978-04-05 traits= skills=Agility=62,BelarusianLanguage=26,Communication=63,Endurance=39,RussianLanguage=64,Strength=45
person f30.c0 Иванова Светлана Владимировна female=True roles=student class=class-classroom-03 born=2000-10-08 traits= skills=Agility=38,Biology=51,Communication=37,Endurance=48,Literature=46,Mathematics=39,PhysicalEducation=44,RussianLanguage=33,Strength=45,UkrainianLanguage=38
person f30.c1 Иванова Надежда Владимировна female=True roles=student class=class-classroom-03 born=2000-12-10 traits=Bully skills=Agility=27,Biology=46,Communication=49,Endurance=61,Literature=36,Mathematics=38,PhysicalEducation=72,RussianLanguage=39,Strength=45,UkrainianLanguage=27
person f30.p0 Иванов Владимир Денисович female=False roles=parent class=- born=1974-06-12 traits=Lazy skills=Agility=40,BelarusianLanguage=26,Biology=37,Communication=72,Endurance=50,Librarianship=45,Physics=66,RussianLanguage=46,Strength=40
person f30.p1 Иванова Мария Андреевна female=True roles=parent class=- born=1973-09-24 traits= skills=Administration=44,Agility=52,Communication=61,Endurance=62,RussianLanguage=46,Strength=45,UkrainianLanguage=30
person f31.c0 Иванов Николай Вадимович female=False roles=student class=class-classroom-01 born=2003-08-29 traits= skills=Agility=41,Biology=43,Communication=32,Endurance=44,Literature=45,Mathematics=53,PhysicalEducation=47,RussianLanguage=7,Strength=45,UkrainianLanguage=21
person f31.p0 Иванов Вадим Андреевич female=False roles=parent class=- born=1985-06-23 traits=Diligent,Quiet skills=Agility=45,BelarusianLanguage=25,Communication=54,Endurance=71,Medicine=57,PhysicalEducation=60,RussianLanguage=56,Strength=53,UkrainianLanguage=25
person f31.p1 Иванова Александра Юриевна female=True roles=parent class=- born=1980-08-20 traits=Diligent skills=Administration=54,Agility=75,BelarusianLanguage=19,Chemistry=43,Communication=65,Cooking=53,Endurance=50,PhysicalEducation=67,RussianLanguage=36,Strength=48,UkrainianLanguage=37
person f32.c0 Петров Артём Евгениевич female=False roles=student class=class-classroom-02 born=2002-07-14 traits=Quiet skills=Agility=31,BelarusianLanguage=34,Biology=33,Communication=5,Endurance=20,Literature=45,Mathematics=21,PhysicalEducation=64,RussianLanguage=42,Strength=80,UkrainianLanguage=25
person f32.c1 Петрова Елена Евгениевна female=True roles=student class=class-classroom-00 born=2003-10-27 traits=Quiet skills=Agility=47,BelarusianLanguage=30,Biology=51,Communication=68,Endurance=48,Literature=46,Mathematics=37,PhysicalEducation=59,RussianLanguage=25,Strength=38,UkrainianLanguage=22
person f32.c2 Петрова Ирина Евгениевна female=True roles=student class=class-classroom-02 born=2001-09-18 traits=HotTempered,Kind skills=Agility=54,BelarusianLanguage=29,Biology=54,Communication=28,Endurance=49,Literature=31,Mathematics=41,PhysicalEducation=67,RussianLanguage=50,Strength=52
person f32.p0 Петров Евгений Станиславович female=False roles=parent class=- born=1963-05-02 traits= skills=Agility=45,BelarusianLanguage=23,Chemistry=61,Communication=66,Endurance=43,Mathematics=49,Pedagogy=34,RussianLanguage=58,Strength=41,UkrainianLanguage=29
person f33.c0 Морозова Елизавета Григориевна female=True roles=student class=class-classroom-00 born=2003-11-04 traits=Diligent,Quiet skills=Agility=21,BelarusianLanguage=25,Biology=38,Communication=41,Endurance=55,Literature=54,Mathematics=53,PhysicalEducation=66,RussianLanguage=42,Strength=45
person f33.p0 Морозов Григорий Михаилович female=False roles=parent class=- born=1968-05-01 traits= skills=Administration=71,Agility=28,BelarusianLanguage=22,Communication=68,Endurance=62,English=53,Librarianship=36,RussianLanguage=52,Strength=51,UkrainianLanguage=14
person f33.p1 Морозова Марина Владимировна female=True roles=parent class=- born=1972-10-24 traits=HotTempered skills=Agility=65,BelarusianLanguage=23,Communication=28,Endurance=40,Pedagogy=57,RussianLanguage=56,Strength=40,UkrainianLanguage=11
person f34.c0 Кузнецова Валентина Ивановна female=True roles=student class=class-classroom-00 born=2004-03-25 traits=Curious,Kind skills=Agility=29,BelarusianLanguage=22,Biology=48,Communication=21,Endurance=69,Literature=30,Mathematics=39,PhysicalEducation=35,RussianLanguage=28,Strength=45,UkrainianLanguage=24
person f34.p0 Кузнецов Иван Сергеевич female=False roles=parent class=- born=1970-02-02 traits= skills=Agility=44,Communication=43,Endurance=58,English=73,Geography=37,Physics=49,RussianLanguage=77,Strength=40
person f34.p1 Кузнецова Дарья Валериевна female=True roles=parent class=- born=1967-03-25 traits=Lazy skills=Administration=60,Agility=69,BelarusianLanguage=23,Communication=67,Cooking=44,Endurance=47,German=38,RussianLanguage=70,Strength=48,UkrainianLanguage=15
person f4.c0 Васильев Денис Федорович female=False roles=student class=class-classroom-01 born=2003-02-01 traits=Neat skills=Agility=50,BelarusianLanguage=6,Biology=39,Communication=27,Endurance=35,Literature=59,Mathematics=36,PhysicalEducation=88,RussianLanguage=47,Strength=21,UkrainianLanguage=13
person f4.p0 Васильев Фёдор Игоревич female=False roles=parent class=- born=1970-05-21 traits=AbsentMinded skills=Agility=66,BelarusianLanguage=31,Communication=49,Cooking=45,Endurance=44,English=36,Literature=48,Pedagogy=62,RussianLanguage=54,Strength=44
person f4.p1 Васильева Анастасия Федоровна female=True roles=parent class=- born=1975-01-20 traits=AbsentMinded skills=Agility=45,BelarusianLanguage=22,Communication=70,Endurance=53,English=43,Librarianship=49,Medicine=75,RussianLanguage=49,Strength=73,UkrainianLanguage=13
person f5.c0 Захарова Екатерина Борисовна female=True roles=student class=class-classroom-02 born=2002-07-13 traits=Kind skills=Agility=43,Biology=46,Communication=44,Endurance=43,Literature=38,Mathematics=52,PhysicalEducation=52,RussianLanguage=45,Strength=32
person f5.p0 Захаров Борис Николаевич female=False roles=parent class=- born=1980-04-15 traits= skills=Agility=45,Communication=37,Endurance=51,English=44,RussianLanguage=57,Strength=29
person f5.p1 Захарова Вероника Григориевна female=True roles=parent class=- born=1979-06-22 traits= skills=Agility=27,Communication=47,Endurance=76,English=24,RussianLanguage=63,Strength=45,UkrainianLanguage=31
person f6.c0 Козловская Юлия Олеговна female=True roles=student class=class-classroom-00 born=2004-08-15 traits= skills=Agility=45,BelarusianLanguage=20,Biology=36,Communication=42,Endurance=61,Literature=49,Mathematics=27,PhysicalEducation=45,RussianLanguage=40,Strength=70
person f6.c1 Козловская Светлана Олеговна female=True roles=student class=class-classroom-02 born=2001-12-31 traits=HotTempered,Leader skills=Agility=53,Biology=47,Communication=36,Endurance=40,Literature=59,Mathematics=52,PhysicalEducation=70,RussianLanguage=36,Strength=40
person f6.c2 Козловская Вероника Олеговна female=True roles=student class=class-classroom-00 born=2003-09-29 traits= skills=Agility=17,BelarusianLanguage=17,Biology=38,Communication=36,Endurance=27,Literature=54,Mathematics=10,PhysicalEducation=51,RussianLanguage=16,Strength=29,UkrainianLanguage=40
person f6.p0 Козловский Олег Станиславович female=False roles=parent class=- born=1967-12-18 traits=Kind skills=Agility=42,Biology=44,Communication=82,Endurance=64,Mathematics=33,RussianLanguage=29,Strength=52
person f6.p1 Козловская Ирина Кирилловна female=True roles=parent class=- born=1963-12-28 traits= skills=Administration=90,Agility=48,BelarusianLanguage=11,Communication=29,Endurance=57,English=48,Librarianship=57,RussianLanguage=48,Strength=61
person f7.c0 Андреев Виктор Степанович female=False roles=student class=class-classroom-01 born=2003-07-19 traits=Bully,Leader skills=Agility=20,Biology=38,Communication=31,Endurance=75,Literature=54,Mathematics=40,PhysicalEducation=60,RussianLanguage=8,Strength=45,UkrainianLanguage=7
person f7.p0 Андреев Степан Андреевич female=False roles=parent class=- born=1973-08-21 traits= skills=Administration=51,Agility=64,BelarusianLanguage=35,Chemistry=42,Communication=76,Endurance=36,Medicine=23,Physics=30,RussianLanguage=67,Strength=46,UkrainianLanguage=27
person f7.p1 Андреева Мария Алексеевна female=True roles=parent class=- born=1977-07-16 traits= skills=Agility=59,Communication=68,Cooking=41,Endurance=49,History=47,Mathematics=54,Medicine=44,RussianLanguage=63,Strength=35,UkrainianLanguage=40
person f8.c0 Фролов Евгений Павелович female=False roles=student class=class-classroom-00 born=2003-12-20 traits=Bully skills=Agility=52,Biology=45,Communication=20,Endurance=78,Literature=61,Mathematics=33,PhysicalEducation=64,RussianLanguage=31,Strength=45,UkrainianLanguage=19
person f8.c1 Фролов Станислав Павелович female=False roles=student class=class-classroom-03 born=2000-10-11 traits=Curious skills=Agility=58,BelarusianLanguage=24,Biology=57,Communication=18,Endurance=69,Literature=55,Mathematics=33,PhysicalEducation=48,RussianLanguage=42,Strength=67,UkrainianLanguage=28
person f8.p0 Фролов Павел Егорович female=False roles=parent class=- born=1968-06-07 traits=Leader skills=Agility=71,BelarusianLanguage=22,Communication=87,Endurance=39,RussianLanguage=64,Strength=44
person f8.p1 Фролова Виктория Степановна female=True roles=parent class=- born=1963-10-04 traits= skills=Agility=59,BelarusianLanguage=25,Communication=51,Endurance=55,Geography=48,Pedagogy=58,RussianLanguage=25,Strength=63
person f9.c0 Кузьмина Любовь Александровна female=True roles=student class=class-classroom-01 born=2003-06-13 traits=Kind skills=Agility=45,BelarusianLanguage=23,Biology=43,Communication=31,Endurance=32,Literature=46,Mathematics=44,PhysicalEducation=62,RussianLanguage=29,Strength=39,UkrainianLanguage=5
person f9.c1 Кузьмин Сергей Александрович female=False roles=student class=class-classroom-03 born=2001-08-20 traits=Curious,Quiet skills=Agility=11,BelarusianLanguage=18,Biology=37,Communication=41,Endurance=59,Literature=59,Mathematics=25,PhysicalEducation=67,RussianLanguage=41,Strength=38
person f9.c2 Кузьмина Алина Александровна female=True roles=student class=class-classroom-03 born=2001-01-22 traits= skills=Agility=52,Biology=50,Communication=29,Endurance=53,Literature=38,Mathematics=52,PhysicalEducation=56,RussianLanguage=54,Strength=31
person f9.p0 Кузьмин Александр Денисович female=False roles=parent class=- born=1981-12-09 traits= skills=Agility=40,Communication=58,Cooking=58,Endurance=78,Literature=42,Medicine=65,Physics=58,RussianLanguage=68,Strength=42
person f9.p1 Кузьмина Анастасия Сергеевна female=True roles=parent class=- born=1977-01-01 traits=AbsentMinded skills=Agility=68,BelarusianLanguage=27,Communication=96,Endurance=46,Librarianship=43,Pedagogy=48,RussianLanguage=61,Strength=40,UkrainianLanguage=21
+57 -5
View File
@@ -1,3 +1,4 @@
using HSchool.Ai;
using HSchool.Content; using HSchool.Content;
using HSchool.People; using HSchool.People;
using HSchool.Schedule; using HSchool.Schedule;
@@ -51,6 +52,44 @@ public class LunchTests
Assert.All(hunger.Values, value => Assert.True(value > 0.5f, $"somebody came back to school at {value:F2}")); Assert.All(hunger.Values, value => Assert.True(value > 0.5f, $"somebody came back to school at {value:F2}"));
} }
/// <summary>
/// Measured once: people arrive full, hit about 0.4 by their own lunch sitting, leave hungry,
/// and come back full the next morning. The numbers live in data; this test is the lock.
/// </summary>
[Fact]
public void Hunger_FollowsTheSchoolDayCurve()
{
using var school = StaffedSchool();
var pupils = school.Roster!.People.Where(person => person.IsStudent).Select(person => person.Id)
.ToHashSet(StringComparer.Ordinal);
var (junior, senior) = Parallels(school);
AdvanceTo(school, new DateTime(2012, 4, 3, 8, 40, 0, DateTimeKind.Utc));
var morning = Hunger(school, pupils, onCampus: true);
Assert.NotEmpty(morning);
Assert.All(morning.Values, value => Assert.InRange(value, 0.85f, 1f));
AdvanceTo(school, new DateTime(2012, 4, 3, 11, 0, 0, DateTimeKind.Utc));
var beforeJuniorLunch = Hunger(school, junior, onCampus: true);
Assert.NotEmpty(beforeJuniorLunch);
Assert.InRange(Median(beforeJuniorLunch.Values), 0.35f, 0.50f);
AdvanceTo(school, new DateTime(2012, 4, 3, 12, 5, 0, DateTimeKind.Utc));
var beforeSeniorLunch = Hunger(school, senior, onCampus: true);
Assert.NotEmpty(beforeSeniorLunch);
Assert.InRange(Median(beforeSeniorLunch.Values), 0.20f, 0.50f);
AdvanceTo(school, new DateTime(2012, 4, 3, 14, 45, 0, DateTimeKind.Utc));
var leaving = Hunger(school, pupils, onCampus: true);
Assert.NotEmpty(leaving);
Assert.True(Median(leaving.Values) < 0.55f, $"median hunger at the last lessons was {Median(leaving.Values):F2}");
AdvanceTo(school, new DateTime(2012, 4, 4, 8, 0, 0, DateTimeKind.Utc));
var nextMorning = Hunger(school, pupils, onCampus: false);
Assert.NotEmpty(nextMorning);
Assert.All(nextMorning.Values, value => Assert.InRange(value, 0.9f, 1f));
}
private static School StaffedSchool() private static School StaffedSchool()
{ {
var (catalog, map) = Vanilla(); var (catalog, map) = Vanilla();
@@ -108,20 +147,33 @@ public class LunchTests
.Select(row => row.PersonId) .Select(row => row.PersonId)
.ToArray(); .ToArray();
private static Dictionary<string, float> Hunger(School school, HashSet<string> pupils) private static Dictionary<string, float> Hunger(School school, HashSet<string> pupils, bool onCampus = false)
{ {
var values = new Dictionary<string, float>(StringComparer.Ordinal); var values = new Dictionary<string, float>(StringComparer.Ordinal);
var query = new Arch.Core.QueryDescription().WithAll<PersonIdentity, PersonNeeds>(); var query = new Arch.Core.QueryDescription().WithAll<PersonIdentity, PersonNeeds, Presence>();
school.World.Query(in query, (ref PersonIdentity identity, ref PersonNeeds needs) => school.World.Query(in query, (ref PersonIdentity identity, ref PersonNeeds needs, ref Presence presence) =>
{ {
if (pupils.Contains(identity.Id) && needs.Values.TryGetValue("Hunger", out var value)) if (!pupils.Contains(identity.Id) || !needs.Values.TryGetValue("Hunger", out var value))
{ {
values[identity.Id] = value; return;
} }
if (onCampus && !presence.IsOnCampus)
{
return;
}
values[identity.Id] = value;
}); });
return values; return values;
} }
private static float Median(IReadOnlyCollection<float> values)
{
var sorted = values.OrderBy(value => value).ToArray();
return sorted[sorted.Length / 2];
}
private static void AdvanceTo(School school, DateTime until) private static void AdvanceTo(School school, DateTime until)
{ {
while (school.Clock.Time < until) while (school.Clock.Time < until)