- Updated `ai.md` to clarify the mechanics of hunger restoration and the importance of lunch breaks in the school schedule. - Revised `schedule.md` to detail the new lunch break structure, allowing for separate sittings for different grade levels. - Enhanced `Decision.cs` and `DecisionPlanner.cs` to incorporate logic for lunch breaks, ensuring that students only leave lessons during their designated lunch windows. - Updated `DayFrameDef` and related classes to support multiple lunch breaks and validate their configurations. - Adjusted tests to validate the new decision-making logic regarding lunch breaks and hunger management, ensuring robust functionality. - Improved localization strings to reflect changes in the school day structure and lunch functionalities.
35 KiB
Люди: генерация, характеристики, просмотр
Договорённость на следующий срез, не текущий код.
Типы и карта: defs.md. Экран игрока: near-term.md.
Потоки: runtime.md. Проекты: projects.md.
Школа перестаёт быть пустой коробкой. Появляются ученики, работники и родители: у каждого имя, тело, навыки, черты и нужды. Ничего из этого пока ни на что не влияет — люди не ходят, не работают и не учатся. Срез отвечает на вопрос «кто в этой школе», а не «что они делают».
Тот же принцип, что и с картой: код — системы, содержание — данные. Новый навык, черта, цвет
волос или набор имён появляются файлом в mods/<id>/defs/, без пересборки.
Откуда берётся состав
Карта уже знает вместимость. ThingDef.PupilSlots × count даёт ученические места в комнате,
RoomDef.positions — должности, которые комната открывает. Отдельного «числа учеников» в форме
создания нет: состав школы — следствие того, что построено.
| Что построено | Что появляется |
|---|---|
| Кабинет с 16 партами | Класс на 16 учеников |
| Кабинет директора | Должность директора и один работник |
| Спортзал | Должность физрука |
| Библиотека | Должность библиотекаря |
Один кабинет — один класс. Кабинет закреплён за классом, как в обычной школе: класс живёт в своём кабинете, и вместимость кабинета есть размер класса. Хотите одиннадцать параллелей — поставьте одиннадцать кабинетов. Это делает редактор карты осмысленным: игрок строит не декорации, а штатное расписание.
Ванильная карта дорабатывается до одиннадцати кабинетов, по одному на параллель, чтобы школа из коробки была полной. Сейчас их четыре.
Класс — сущность ростера, а не поле карты
Параллель и литера — именно поля, а не разбор подписи комнаты. Подпись разбирать нельзя дважды:
кириллическая «А» и латинская «A» на глаз неразличимы (в нынешней ванильной карте кабинеты
подписаны латиницей — 1A, 2B), а в другой локали литеры вообще другие.
Но и в карте этим полям не место. Параллель растёт каждый год: 5Б становится 6Б, не переезжая из кабинета. Раскладка же после создания школы неизменна — иначе файл карты пришлось бы переписывать каждое игровое 1 сентября.
Поэтому класс — отдельная сущность ростера: параллель, литера, закреплённый кабинет. Кабинет закреплён за классом на всё время его жизни; одиннадцатый выпускается — кабинет достаётся новому первому. Раскладку по параллелям при создании делает генератор, игрок её не задаёт.
Из этого следует, что подписи кабинетов в ванильной карте — номера помещений («101», «204»), как в настоящей школе, а не имена классов. Имя класса живёт на классе и меняется вместе с ним.
Из чего состоит человек
Шесть слоёв. Первые два есть у всех, остальные — по роли.
| Слой | Что это | Пример |
|---|---|---|
| Личность | ФИО со склонениями, пол, дата рождения, роль | Иванова Мария Петровна, ж, 2001-03-14 |
| Тело | Рост, вес, цвет волос, цвет глаз, телосложение | 164 см, 52 кг, русые, серые, худощавое |
| Навыки | Числовые шкалы | Математика 62, Ловкость 31 |
| Черты | Дискретные, с весами и несовместимостями | Усидчивый, Задира |
| Нужды | Числовые шкалы, убывают со временем | Сон 0.7, Голод 0.4 |
| Связи | Семья: родители, дети, братья и сёстры | дочь Иванова П.С. и Ивановой О.М. |
Роли три: ученик, работник, родитель. Роль — не def: код различает их по существу,
а не по данным. Должность работника — PositionDef, он уже есть.
Один человек может быть в двух ролях сразу: учительница математики, чей сын учится в седьмом классе, — это один человек с ролью работника и связью «родитель» к ученику. Отдельной сущности «родитель Иванова» для неё не заводится.
Родители
Родители — полноценные сущности в мире школы, но без места на карте. Они существуют, потому что у ученика есть семья, а не потому что ходят по коридорам.
Это задел, а не упрощение: позже родителя можно будет вызвать в школу — тогда у него появится
местоположение и цель, но не появится новый тип сущности. Поэтому родитель с самого начала лежит
в World наравне с учеником, а не отдельным списком «карточек при ученике».
Генерация семьями
Люди генерируются не по одному. Единица генерации — семья:
Семья Ивановых
├── Иванов Пётр Сергеевич родитель, 41 год
├── Иванова Ольга Михайловна родитель, 39 лет
├── Иванова Мария Петровна ученица, 7Б
└── Иванов Кирилл Петрович ученик, 3А
Что это даёт бесплатно и чего иначе не получить:
- Фамилии повторяются. Школа без братьев и сестёр читается как список случайных строк.
- Отчества сходятся. Отчество ребёнка — от имени отца, а не от отдельного словаря.
- Родовые формы согласованы. Иванов / Иванова в одной семье, а не вразнобой.
- Работник может быть родителем ученика — фактом семьи, без специального правила.
Генератор идёт сверху вниз: сначала решает, сколько семей нужно, чтобы заполнить места и должности; потом строит взрослых; потом детей нужных возрастов; и только затем раскладывает детей по классам, а взрослых — по должностям. Обратный порядок («сгенерировать 400 учеников, потом приписать им родителей») не даёт ни братьев, ни учителей-родителей.
Места раздаются вперемешку по всей школе, а не подряд. Карта отдаёт ученические места сгруппированными по кабинетам, и семья, берущая подряд идущие места, получала бы всех детей в один класс — то есть ровесниками, с одним и тем же окном рождения. Порядок мест перемешивается от школьного сида до раздачи.
Цена решения названа прямо: набор мест перестаёт быть «дописываемым с конца». Более крупная карта даёт другую раскладку по классам с самого начала, а не только в хвосте. Что остаётся неизменным — как разыгрывается сама семья: её сид зависит только от номера, поэтому фамилия, состав и имена семьи №5 одинаковы в школе на четыре кабинета и в школе на пять. Меняется то, в каком классе сидят её дети, и вместе с классом — год рождения.
Неполные семьи
Примерно 8% семей с детьми живут с одним родителем. Кто именно остался — отец или мать — решается броском, без перевеса в чью-либо сторону.
Причина не моделируется. Ростер записывает, кто живёт в доме, а не почему. Развод, вдовство и прочее — это события, а событий в этом срезе нет; появится лента — появится и повод завести признак.
Ребёнок в любом случае носит отцовскую фамилию и отчество. Значит, отсутствующему отцу всё равно разыгрывается имя — оно записывается на семью вместе с фамилией. Читать их с присутствующего родителя нельзя: у матери-одиночки нет мужской формы фамилии, а сыну нужна именно она.
Решение «кто остался» берётся из отдельного потока, а не из общего с внешностью. В общем потоке два соседних броска оказались связаны, и все неполные семьи до единой вышли материнскими.
Номер ребёнка в идентификаторе — счётчик, а не длина списка. Выпускник уходит из ростера; если следующий ребёнок семьи получит номер по числу оставшихся детей, он унаследует идентификатор ушедшего, и все ссылки на него — семейные связи, открытая карточка — молча начнут показывать другого человека.
Дефы
Пять новых видов. Ссылки — строками defName, как везде.
- SkillDef — навык. Диапазон, распределение при генерации, зависимость от возраста,
ограничения со стороны тела (см. ниже). Не каждый человек получает каждый навык:
always— у всех, предметы — по параллели ученика,work— только взрослым,adultChance— шанс у взрослого поверх родного языка. - TraitDef — черта. Вес (частота), список несовместимых черт, ограничения по роли и возрасту, модификаторы навыков.
- BodyAttributeDef — свойство тела. Либо число с распределением (рост, вес), либо выбор из списка с весами (цвет волос, цвет глаз). Распределение зависит от пола и возраста.
- NeedDef — нужда. Начальное значение и скорость убывания.
- NameSetDef — набор имён (см. отдельный раздел).
Телосложение — не отдельный def, а производное: считается из роста и веса кодом. Данные не должны дублировать то, что выводится.
Что кладём в core
| Вид | Ванильное содержание |
|---|---|
| Навыки | Школьные предметы, языки набора имён, общение, физические (ловкость, сила, выносливость) и рабочие (педагогика, медицина, кухня…) |
| Черты | Около десяти для начала: усидчивый, рассеянный, задира, тихоня, лидер, лентяй, любопытный, вспыльчивый, добрый, аккуратный |
| Тело | Рост, вес, цвет волос, цвет глаз |
| Нужды | Сон, голод, туалет, общение |
Слои одинаковы у всех ролей: у родителя те же виды данных, что у ученика. Набор ключей
навыков — нет. Нет ключа — нет навыка, а не ноль: карточка его не показывает, урок, если
предмет всё же начался, стартует с Range.Min. Иначе первоклассник носил бы химию «для
полноты», а код всё равно спрашивал бы, есть ли она.
Как тело ограничивает навыки
«Толстый не может быть ловким» — правило генерации, а не игровой эффект: характеристики пока ни на что не влияют, но между собой согласованы.
Ограничение живёт в SkillDef и ссылается на свойство тела:
{
"defName": "Agility",
"range": { "min": 0, "max": 100 },
"bodyLimits": [
{ "attribute": "Build", "value": "Obese", "max": 25 },
{ "attribute": "Build", "value": "Heavy", "max": 45 },
{ "attribute": "Build", "value": "Athletic", "min": 40 },
],
}
Генератор сначала бросает тело, потом те навыки, которые человеку положены, потом применяет ограничения как зажим. Порядок важен и фиксируется: тело первично, навык подстраивается. Иначе один и тот же сид даст разных людей в зависимости от порядка перебора.
Выдача навыков тоже фиксирована: always → родной язык школы → предметы параллели (ученик) →
лишние академические, рабочие и adultChance (взрослый) → черты и зажим тела → родственные
языки набора. Годовой набор добавляет только новые ключи предметов; уже выданные значения не
перебрасываются.
Ограничения — не только вниз: «атлет не может быть совсем неловким» так же осмысленно, поэтому
min есть наравне с max.
Нужды
Четыре нужды в core: сон, голод, туалет, общение. NeedDef описывает шкалу и скорость
убывания. Машинерия работает с этого среза: значение лежит на человеке, тикает вместе со школой,
показывается в карточке.
Но в core скорость убывания равна нулю. Есть тела, которые тратят, и нет ничего, что
восполняет: некому поесть, негде поспать. Ненулевая скорость в этом срезе означала бы школу, где
через игровые сутки все нужды на нуле, — и это выглядело бы поломкой, а не игрой.
Скорость — данные, не код. Когда появится еда и сон, цифра меняется в JSONC, а не в C#.
Имена
NameSetDef — набор имён целиком: мужские имена, женские имена, фамилии, правила отчеств и
склонений. В core один набор — славянский. Другие наборы приходят модами.
Один набор на школу. Выбирается в диалоге создания рядом с модами и сохраняется в файле школы. Смеси с весами («80% славянских, 20% прочих») — не в этом срезе: они требуют UI для весов.
Набор несёт список языков, на которых здесь говорят (nativeLanguages). Школа выбирает один
при создании — это родной язык всех людей. Остальные языки списка часто появляются на низком
уровне (relatedLanguageChance и соседние поля на том же def): русскоязычный может понимать
белорусский. Уже выданный ключ — родной или предмет школы — не перезаписывается, поэтому
белорусский ученик сохраняет школьный русский, а не обрезанный «чуть-чуть». Шанс 0 оставляет
родственников выключенными (одноязычный пакет).
Набор имён не связан с языком интерфейса. Английский UI не превращает Иванову в Ivanova: язык интерфейса — про подписи кнопок, набор имён — про то, кто учится в школе.
Склонения
Русскому тексту нужны падежи: «вызвать Иванову Марию», «дневник Ивановой Марии». Шесть падежей на каждую часть ФИО.
Два способа, и нужны оба:
- Правило — по умолчанию. Код знает небольшой набор моделей склонения (
-ов/-ова,-ий/-ая,-а/-я, несклоняемые). В данных — только имя и id модели. - Явная таблица — когда правило не работает. Любая запись может задать все шесть форм руками.
Это тот же принцип, что и везде: код знает как, данные говорят что. Словарь из шести форм на каждое из тысячи имён никто не выдержит, а одними правилами русские фамилии не покрываются.
Падежи нужны заранее, до появления ленты событий: набор имён с одной формой потом придётся
переписывать целиком, а он самый объёмный файл в core.
Отдельная библиотека
Новый проект HSchool.People.
Protocol ← Server → Simulation → People → Content
- Зависит от
HSchool.Content(нужны defs и каталог). - Не зависит от Arch, ASP.NET и сокетов — как
Contentсейчас. - Отдаёт простые записи. Превращает их в сущности
WorldужеHSchool.Simulation.
Так генератор тестируется без мира и без хоста: «этот сид + эта карта + этот набор имён + этот родной язык → ровно эти люди». Это единственный способ поймать регрессию в генераторе — глазами такое не проверяется.
Детерминизм
Генерация детерминирована от сида, сид лежит в файле школы. Один сид + одна карта + один набор имён + один родной язык = одна и та же школа, всегда. Если родной язык при создании не указан, его выбирает тот же сид — повтор без поля даёт тот же язык.
Сид у каждой семьи свой, выведенный из школьного: добавление тринадцатой семьи не должно менять первые двенадцать. Иначе тест на генератор ломается от любой правки порядка.
Годовой набор
Состав живёт: 1 сентября классы переходят в следующую параллель, одиннадцатый выпускается, его кабинет освобождается под новый первый.
Вместимость при этом постоянна — она задана картой. Набор не увеличивает школу, а замещает выпуск. Школа растёт только стройкой.
Выпускается старшая имеющаяся параллель: школа из четырёх кабинетов не должна ждать одиннадцатого класса, чтобы кого-то выпустить.
Выпускники уходят из ростера. Их родители уходят следом, но только если в школе не осталось других их детей и сами они в ней не работают, — иначе ростер копил бы людей, ни с чем не связанных, до бесконечности.
Школа создаётся посреди учебного года (по умолчанию 3 апреля), поэтому при создании классы уже сформированы и возраст ученика соответствует его параллели: в N-й класс ходят те, кому исполнилось N+6 до 1 сентября. Переход в следующую параллель добавляет навыки новых предметов (английский с пятого, химия с восьмого) и не перебрасывает уже выданные.
Просмотр
Не отдельный экран. Список людей — панель в оболочке менеджера, рядом с картой: уходя в отдельный экран, игрок теряет из виду часы и локацию, а смысл этой оболочки ровно в том, чтобы всё было на одном экране. Ориентир — прогрессивный QSP: текст, списки и ссылки, без сцены.
Панель «Люди» — вторая вкладка той же панели, что и карта: фильтрам, колонкам и пейджеру нужна ширина, а держать список третьим рядом под картой значило бы отдать ему высоту, которой на экране школы нет. Вкладка переключается рядом с деревом, выбранная строка открывает карточку справа — там же, где содержимое локации.
- Фильтры: роль (ученики / работники / родители), параллель и литера, должность, пол, возрастной диапазон.
- Сортировки: фамилия, возраст, параллель, должность.
- Пейджинг обязателен: тысяча учеников — это три тысячи человек с семьями.
- Строка списка — только лёгкое: ФИО, роль, класс или должность, возраст, пол.
- Карточка одного человека — тело, навыки (только те, что есть), черты, нужды, семья со ссылками на родных.
Масштаб
Школа на 1000 учеников — это около 3000 человек вместе с родителями, и до шести таких школ живут одновременно. Три решения следуют отсюда напрямую.
Ростер не ходит в снимке карты. Снимок при открытии школы остаётся про помещения. Люди — отдельный путь, иначе открытие школы означало бы мегабайт по сокету.
Список — HTTP, а не сокет. Запрос-ответ с фильтром, сортировкой и страницей — это REST по природе, как и меню. Воркер публикует неизменяемый снимок ростера (он меняется редко: создание, набор, наём), обработчик читает опубликованный снимок и не трогает воркер — инвариант 3 цел.
Живые значения — из воркера. Нужды тикают, поэтому карточка одного человека запрашивается у
воркера через мейлбокс и TaskCompletionSource, как create и delete. Один человек — дешёвый
запрос; три тысячи людей двадцать раз в секунду — нет.
Ростер — отдельный файл. saves/{id}.people.json пишется только когда состав изменился;
saves/{id}.json остаётся маленьким и частым. Сейчас файл школы переписывается каждые 30 секунд —
таскать через это мегабайт ростера, который меняется раз в игровой год, незачем.
Советы, которые стоит принять сразу
- Не делать «родителя» отдельным видом сущности. Иначе учительница-мать станет двумя людьми.
- Не хранить телосложение в данных — оно выводится из роста и веса.
- Не начинать с падежных таблиц на каждое имя: правило по умолчанию, таблица как исключение.
- Не отправлять черты и навыки в списке — только в карточке. Список читают глазами, карточку открывают по одному человеку.
- Сид на семью, а не один на школу: иначе любая правка генератора переставит всех.
- Ненулевую скорость нужд не включать, пока нечем их восполнять.
Заведомо не сейчас
- Поведение: расписание, уроки, перемещение по карте, исполнение действий.
- Влияние характеристик на что-либо. Черты и навыки в этом срезе — текст в карточке.
- Вызов родителя в школу. Задел есть (родитель — сущность), реализации нет.
- Наём и увольнение вручную. Должности закрываются генератором.
- Смеси наборов имён с весами.
- Новые роли из модов: ученик, работник, родитель — код, не данные.
- Портреты и любой арт.
- Причина неполной семьи: развод, вдовство и прочее — это события, а ленты событий ещё нет.
Зафиксировано этим разговором
| Тема | Решение |
|---|---|
| Размер школы | Из карты: PupilSlots — ученики, RoomDef.positions — штат |
| Класс | Один кабинет = один класс; класс — сущность ростера, не поле карты |
| Параллель и литера | Поля класса, а не разбор подписи комнаты; раскладывает генератор |
| Подписи кабинетов | Номера помещений («204»), имя класса живёт на классе |
| Ванильная карта | Дорабатывается до 11 кабинетов — по одному на параллель |
| Когда генерируются | Полный состав при создании, затем набор каждое 1 сентября |
| Годовой набор | Замещает выпуск; вместимость меняет только стройка |
| Кого выпускать | Старшую имеющуюся параллель, а не обязательно одиннадцатую |
| Уход из ростера | Выпускники уходят; родители — если не осталось детей и они здесь не работают |
| Роли | Ученик, работник, родитель; роль — код, должность — PositionDef |
| Родители | Сущности в World, без места на карте; задел на вызов в школу |
| Совмещение ролей | Один человек может быть работником и родителем ученика |
| Единица генерации | Семья, а не человек |
| Неполные семьи | Около 8% семей с детьми; остаётся случайный родитель; причина не моделируется |
| Фамилия и отчество | Всегда отцовские; имя отца и фамилия записаны на семье, а не на человеке |
| Раздача мест | Вперемешку по школе от сида: иначе братья и сёстры — всегда одноклассники |
| Стабильность сида | Гарантируется по семье (фамилия, состав, имена), не по месту в классе |
| Идентификатор ребёнка | Счётчик семьи; номер выпустившегося не переиспользуется |
| Набор в первый класс | Новые семьи приходят по одному ребёнку — иначе это тройняшки-ровесники |
| Порядок ограничений | Черта не может пробить потолок тела: зажим повторяется после модификаторов |
| Слои человека | Личность, тело, навыки, черты, нужды, связи |
| Новые defs | SkillDef, TraitDef, BodyAttributeDef, NeedDef, NameSetDef |
| Телосложение | Производное от роста и веса, не def |
| Тело и навыки | SkillDef.bodyLimits зажимают навык; тело бросается первым |
| Влияние характеристик | Никакого; только просмотр |
Навыки в core |
Предметы, языки, общение, физические, рабочие |
| Выдача навыков | Не полный каталог: always, родной, предметы года, взрослые лишние, родственные языки |
Черты в core |
Около десяти |
Нужды в core |
Сон, голод, туалет, общение |
| Нужды | Машинерия есть, скорость убывания в core — ноль |
| Слои по ролям | Одинаковые виды данных; ключи навыков зависят от роли и возраста |
| Набор имён | Один на школу, выбор в create, в core славянский; прочие — моды |
| Родной язык | Один из nativeLanguages набора, выбор в create; остальные языки списка — часто низкий навык |
| Падежи | Правило по умолчанию (код), явная таблица как исключение (данные) |
| Имена и язык UI | Независимы |
| Библиотека | HSchool.People: зависит от Content, не знает Arch и ASP.NET |
| Детерминизм | Сид в сейве, свой сид на семью; родной язык — часть входа |
| Просмотр | Панель в оболочке менеджера, вкладкой рядом с картой; не отдельный экран |
| Список | Фильтры, сортировки, пейджинг; в строке — только лёгкие поля |
| Транспорт списка | HTTP по опубликованному снимку ростера |
| Транспорт карточки | Запрос в воркер, потому что нужды живые |
| Сохранение | saves/{id}.people.json, пишется при изменении состава |