Merge branch 'phase/51-warmth-lesson'
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -107,9 +107,8 @@
|
|||||||
Сдвиг — поле на `TraitDef` (смещение комфортной температуры), не список фамилий в коде. Мод
|
Сдвиг — поле на `TraitDef` (смещение комфортной температуры), не список фамилий в коде. Мод
|
||||||
добавляет «мерзлявый» тем же полем.
|
добавляет «мерзлявый» тем же полем.
|
||||||
|
|
||||||
Голодный учится хуже уже сейчас. Замёрзший в этом срезе **ещё не** получает штраф к уроку: нужда
|
Голодный учится хуже уже сейчас. Замёрзший — тоже: та же `NeedFactor`, что у голода. Нужда
|
||||||
видна и двигает переодевание, но таблицу оценок мы не заводим. Когда появятся последствия состояния,
|
видна, двигает переодевание и режет рост навыка на уроке. Оценок как журнала нет.
|
||||||
тепло встанет рядом с голодом.
|
|
||||||
|
|
||||||
## Один ThingDef, три роли
|
## Один ThingDef, три роли
|
||||||
|
|
||||||
@@ -386,7 +385,7 @@
|
|||||||
|
|
||||||
- Магазины, деньги за одежду, стирка, кража, выдача со склада игроком.
|
- Магазины, деньги за одежду, стирка, кража, выдача со склада игроком.
|
||||||
- Игрок одевает человека с карточки.
|
- Игрок одевает человека с карточки.
|
||||||
- Штраф к уроку от холода, болезни от промокания, прогул «не в форме» как оценка.
|
- Болезни от промокания, прогул «не в форме» как оценка. Штраф к уроку от холода — фаза 51.
|
||||||
- Смена страны или климатического пресета у живой школы.
|
- Смена страны или климатического пресета у живой школы.
|
||||||
- Почасовая анимация погоды, ветер, грязь, зонты как система.
|
- Почасовая анимация погоды, ветер, грязь, зонты как система.
|
||||||
- Шкафчики сотрудников, раздевалки в каждом корпусе сверх двух ванильных.
|
- Шкафчики сотрудников, раздевалки в каждом корпусе сверх двух ванильных.
|
||||||
@@ -428,4 +427,4 @@
|
|||||||
| История | Только этот человек, только сегодня, HTTP, поиск и пейджинг |
|
| История | Только этот человек, только сегодня, HTTP, поиск и пейджинг |
|
||||||
| Инвентарь в сейве | На человеке в `people.json`, писать при содержательной смене |
|
| Инвентарь в сейве | На человеке в `people.json`, писать при содержательной смене |
|
||||||
| Игрок надевает | Нет |
|
| Игрок надевает | Нет |
|
||||||
| Погода влияет на ИИ | Да: переодеться. Ходьбу и урок в этом срезе не штрафует |
|
| Погода влияет на ИИ | Да: переодеться. Ходьбу не штрафует. Урок — через нужду тепла |
|
||||||
|
|||||||
@@ -11,21 +11,21 @@
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] `Gain` умножает `NeedFactor(голод) × NeedFactor(тепло)`
|
- [x] `Gain` умножает `NeedFactor(голод) × NeedFactor(тепло)`
|
||||||
- [ ] Нет нужды `Warmth` в каталоге — множитель тепла 1, голод как сейчас
|
- [x] Нет нужды `Warmth` в каталоге — множитель тепла 1, голод как сейчас
|
||||||
- [ ] Лог `lesson-cold` один раз на предмет в сутки, если на уроке, который иначе учил,
|
- [x] Лог `lesson-cold` один раз на предмет в сутки, если на уроке, который иначе учил,
|
||||||
тепло ниже `BehaviorDef.needThreshold`; `thingDef` — предмет
|
тепло ниже `BehaviorDef.needThreshold`; `thingDef` — предмет
|
||||||
- [ ] `docs/protocol.md` — тип лога
|
- [x] `docs/protocol.md` — тип лога
|
||||||
- [ ] В [`inventory.md`](../design/inventory.md) убрать «тепло урок не штрафует»: стало рядом
|
- [x] В [`inventory.md`](../design/inventory.md) убрать «тепло урок не штрафует»: стало рядом
|
||||||
с голодом
|
с голодом
|
||||||
|
|
||||||
## Тесты, без которых фаза не закрыта
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
- [ ] Тепло 0.1 и голод 1 → меньший прирост, чем оба по 1
|
- [x] Тепло 0.1 и голод 1 → меньший прирост, чем оба по 1
|
||||||
- [ ] Тепло 1 и голод 0.1 → как нынешний голодный, тепло не «вылечивает»
|
- [x] Тепло 1 и голод 0.1 → как нынешний голодный, тепло не «вылечивает»
|
||||||
- [ ] Нет `Warmth` в каталоге — голодный тест фазы 21 по-прежнему зелёный
|
- [x] Нет `Warmth` в каталоге — голодный тест фазы 21 по-прежнему зелёный
|
||||||
- [ ] За урок на холоде одна строка лога, не по числу тиков
|
- [x] За урок на холоде одна строка лога, не по числу тиков
|
||||||
- [ ] Учителя нет в кабинете — лога холода нет (урок не учил)
|
- [x] Учителя нет в кабинете — лога холода нет (урок не учил)
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,6 @@
|
|||||||
| [48. Учитель в кабинете](48-lesson-teacher-present.md) | ✅ | Нет учителя на месте — нет роста навыка |
|
| [48. Учитель в кабинете](48-lesson-teacher-present.md) | ✅ | Нет учителя на месте — нет роста навыка |
|
||||||
| [49. Пропуск сборки при запуске](49-skip-stale-build.md) | ✅ | `run-aspire.cmd` не гоняет MSBuild, если dll свежие |
|
| [49. Пропуск сборки при запуске](49-skip-stale-build.md) | ✅ | `run-aspire.cmd` не гоняет MSBuild, если dll свежие |
|
||||||
| [50. Навык учителя](50-teacher-skill-gain.md) | ✅ | Соискатель 78 учит лучше, чем 41 |
|
| [50. Навык учителя](50-teacher-skill-gain.md) | ✅ | Соискатель 78 учит лучше, чем 41 |
|
||||||
| [51. Тепло на уроке](51-warmth-lesson.md) | 🔄 | Замёрзший учится хуже, как голодный |
|
| [51. Тепло на уроке](51-warmth-lesson.md) | ✅ | Замёрзший учится хуже, как голодный |
|
||||||
| [52. Учебник на уроке](52-textbook-lesson.md) | ⬜ | Нет в сумке — половинный рост |
|
| [52. Учебник на уроке](52-textbook-lesson.md) | ⬜ | Нет в сумке — половинный рост |
|
||||||
| [53. Погода на дороге](53-weather-commute.md) | ⬜ | Снег и дождь добавляют минуты к приходу |
|
| [53. Погода на дороге](53-weather-commute.md) | ⬜ | Снег и дождь добавляют минуты к приходу |
|
||||||
|
|||||||
+3
-2
@@ -416,8 +416,9 @@ arrive in one response.
|
|||||||
|
|
||||||
Row `type` values: `action-started`, `action-ended`, `apparel-replaced` (morning issue, when
|
Row `type` values: `action-started`, `action-ended`, `apparel-replaced` (morning issue, when
|
||||||
phase 34 appends it), `apparel-changed` (dressing, when phase 35 appends it),
|
phase 34 appends it), `apparel-changed` (dressing, when phase 35 appends it),
|
||||||
`lesson-no-teacher` (the assigned teacher was not standing in the lesson room). `thingDef` is
|
`lesson-no-teacher` (the assigned teacher was not standing in the lesson room),
|
||||||
the action, apparel or subject def the caption was built from.
|
`lesson-cold` (warmth below the behaviour threshold during a lesson that otherwise taught).
|
||||||
|
`thingDef` is the action, apparel or subject def the caption was built from.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ using HSchool.Content;
|
|||||||
namespace HSchool.Ai;
|
namespace HSchool.Ai;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much a lesson adds to one skill this step. Hungry learns worse; trait offsets and the
|
/// How much a lesson adds to one skill this step. Hungry or cold learns worse; trait offsets and
|
||||||
/// teacher's subject skill scale the rate. The world stores the running total — this is just
|
/// the teacher's subject skill scale the rate. Missing warmth is treated as 1.
|
||||||
/// the number.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class LessonLearning
|
public static class LessonLearning
|
||||||
{
|
{
|
||||||
@@ -54,13 +53,15 @@ public static class LessonLearning
|
|||||||
float hours,
|
float hours,
|
||||||
float hunger,
|
float hunger,
|
||||||
int traitOffset,
|
int traitOffset,
|
||||||
float teacherSkill)
|
float teacherSkill,
|
||||||
|
float warmth = 1f)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(skill);
|
ArgumentNullException.ThrowIfNull(skill);
|
||||||
var delta = share
|
var delta = share
|
||||||
* lessonSkillPerHour
|
* lessonSkillPerHour
|
||||||
* hours
|
* hours
|
||||||
* NeedFactor(hunger)
|
* NeedFactor(hunger)
|
||||||
|
* NeedFactor(warmth)
|
||||||
* TraitFactor(traitOffset)
|
* TraitFactor(traitOffset)
|
||||||
* TeacherFactor(teacherSkill);
|
* TeacherFactor(teacherSkill);
|
||||||
return Math.Clamp(current + delta, skill.Range.Min, skill.Range.Max);
|
return Math.Clamp(current + delta, skill.Range.Min, skill.Range.Max);
|
||||||
|
|||||||
@@ -205,5 +205,6 @@
|
|||||||
"ApparelReplaced": "got a new {0}",
|
"ApparelReplaced": "got a new {0}",
|
||||||
"ApparelChanged": "changed clothes: {0}",
|
"ApparelChanged": "changed clothes: {0}",
|
||||||
"LessonNoTeacher": "lesson without a teacher: {0}",
|
"LessonNoTeacher": "lesson without a teacher: {0}",
|
||||||
|
"LessonCold": "too cold in class: {0}",
|
||||||
"core": "Core",
|
"core": "Core",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,5 +205,6 @@
|
|||||||
"ApparelReplaced": "получил новую {0}",
|
"ApparelReplaced": "получил новую {0}",
|
||||||
"ApparelChanged": "переоделся: {0}",
|
"ApparelChanged": "переоделся: {0}",
|
||||||
"LessonNoTeacher": "урок без учителя: {0}",
|
"LessonNoTeacher": "урок без учителя: {0}",
|
||||||
|
"LessonCold": "замёрз на уроке: {0}",
|
||||||
"core": "Базовая игра",
|
"core": "Базовая игра",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ internal static class LessonLearningSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hunger = needs.Values.GetValueOrDefault("Hunger", 1f);
|
var hunger = needs.Values.GetValueOrDefault("Hunger", 1f);
|
||||||
|
var warmth = needs.Values.GetValueOrDefault("Warmth", 1f);
|
||||||
|
if (warmth < rules.NeedThreshold)
|
||||||
|
{
|
||||||
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonCold, lesson.Subject);
|
||||||
|
}
|
||||||
|
|
||||||
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
||||||
? found
|
? found
|
||||||
: new Dictionary<string, float>(StringComparer.Ordinal);
|
: new Dictionary<string, float>(StringComparer.Ordinal);
|
||||||
@@ -108,7 +114,8 @@ internal static class LessonLearningSystem
|
|||||||
hours,
|
hours,
|
||||||
hunger,
|
hunger,
|
||||||
TraitOffset(catalog, traits, share.Skill),
|
TraitOffset(catalog, traits, share.Skill),
|
||||||
teacherSkill);
|
teacherSkill,
|
||||||
|
warmth);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public static class PersonLogTypes
|
|||||||
public const string ApparelReplaced = "apparel-replaced";
|
public const string ApparelReplaced = "apparel-replaced";
|
||||||
public const string ApparelChanged = "apparel-changed";
|
public const string ApparelChanged = "apparel-changed";
|
||||||
public const string LessonNoTeacher = "lesson-no-teacher";
|
public const string LessonNoTeacher = "lesson-no-teacher";
|
||||||
|
public const string LessonCold = "lesson-cold";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -60,12 +61,16 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
|||||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal))
|
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|
||||||
|
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var name = catalog.Subjects.TryGetValue(ThingDef, out var subject)
|
var name = catalog.Subjects.TryGetValue(ThingDef, out var subject)
|
||||||
? catalog.Label(locale, subject)
|
? catalog.Label(locale, subject)
|
||||||
: ThingDef;
|
: ThingDef;
|
||||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "LessonNoTeacher"), name);
|
var key = Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
||||||
|
? "LessonCold"
|
||||||
|
: "LessonNoTeacher";
|
||||||
|
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal))
|
if (Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -67,6 +67,36 @@ public class LessonLearningTests
|
|||||||
Assert.NotEqual(skills.Values.First(), average);
|
Assert.NotEqual(skills.Values.First(), average);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ColdLearnsLessThanWarm()
|
||||||
|
{
|
||||||
|
var warm = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 1f, 0, 100, warmth: 1f);
|
||||||
|
var cold = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 1f, 0, 100, warmth: 0.1f);
|
||||||
|
|
||||||
|
Assert.True(cold > 50);
|
||||||
|
Assert.True(warm - 50 > cold - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HungerStillCutsWhenWarm()
|
||||||
|
{
|
||||||
|
var full = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, hunger: 1f, traitOffset: 0, teacherSkill: 100, warmth: 1f);
|
||||||
|
var hungry = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, hunger: 0.1f, traitOffset: 0, teacherSkill: 100, warmth: 1f);
|
||||||
|
|
||||||
|
Assert.True(full - 50 > hungry - 50);
|
||||||
|
Assert.Equal(
|
||||||
|
LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 0.1f, 0, 100),
|
||||||
|
hungry);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingWarmthArgument_DefaultsToFull()
|
||||||
|
{
|
||||||
|
var implied = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 0.1f, 0, 100);
|
||||||
|
var explicitWarm = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 0.1f, 0, 100, warmth: 1f);
|
||||||
|
|
||||||
|
Assert.Equal(explicitWarm, implied);
|
||||||
|
}
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MissingTeacherSkill_UsesRangeMin_AndStillGains()
|
public void MissingTeacherSkill_UsesRangeMin_AndStillGains()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -190,6 +190,54 @@ public class LessonTeacherPresentTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cold_LogsOnce_AndGainsLess()
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, homeroom);
|
||||||
|
var classmate = school.Roster!.Classes.First(row => row.PupilIds.Contains(pupilId))
|
||||||
|
.PupilIds.First(id => id != pupilId);
|
||||||
|
SetPlace(school, classmate, homeroom);
|
||||||
|
SetNeed(school, pupilId, "Warmth", 0.1f);
|
||||||
|
SetNeed(school, classmate, "Warmth", 1f);
|
||||||
|
var coldBefore = SkillOf(school, pupilId, "Mathematics");
|
||||||
|
var warmBefore = SkillOf(school, classmate, "Mathematics");
|
||||||
|
|
||||||
|
LessonLearningSystem.Apply(school, 5);
|
||||||
|
LessonLearningSystem.Apply(school, 5);
|
||||||
|
|
||||||
|
var rows = school.DayLog
|
||||||
|
.Where(row => row.PersonId == pupilId && row.Type == PersonLogTypes.LessonCold)
|
||||||
|
.ToArray();
|
||||||
|
Assert.Single(rows);
|
||||||
|
Assert.Equal("Mathematics", rows[0].ThingDef);
|
||||||
|
Assert.Equal("замёрз на уроке: Математика", rows[0].Caption(school.Catalog!, "ru"));
|
||||||
|
Assert.True(SkillOf(school, classmate, "Mathematics") - warmBefore
|
||||||
|
> SkillOf(school, pupilId, "Mathematics") - coldBefore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cold_DoesNotLog_WhenTeacherIsAbsent()
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, "restroom-1");
|
||||||
|
SetNeed(school, pupilId, "Warmth", 0.1f);
|
||||||
|
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(school.DayLog, row => row.Type == PersonLogTypes.LessonCold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static (School School, string Homeroom, string PupilId, string TeacherId) StaffedMath(
|
private static (School School, string Homeroom, string PupilId, string TeacherId) StaffedMath(
|
||||||
string? teacherId = null)
|
string? teacherId = null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user