Halve lesson gain when the bag has no textbook for the subject.
Locker and home do not count. A pack without the field keeps vanilla 0.5. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,20 +12,20 @@
|
|||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
- [ ] Рост урока полный, только если в **сумке** есть экземпляр с `subject` этого предмета
|
- [x] Рост урока полный, только если в **сумке** есть экземпляр с `subject` этого предмета
|
||||||
- [ ] Шкафчик и дом не считаются: ученик в кабинете
|
- [x] Шкафчик и дом не считаются: ученик в кабинете
|
||||||
- [ ] Нет учебника — множитель `BehaviorDef.lessonNoTextbookFactor`, ваниль 0.5
|
- [x] Нет учебника — множитель `BehaviorDef.lessonNoTextbookFactor`, ваниль 0.5
|
||||||
- [ ] Валидатор: число 0–1
|
- [x] Валидатор: число 0–1
|
||||||
- [ ] Лог `lesson-no-textbook` один раз на предмет в сутки; `thingDef` — предмет
|
- [x] Лог `lesson-no-textbook` один раз на предмет в сутки; `thingDef` — предмет
|
||||||
- [ ] `docs/protocol.md` — тип лога
|
- [x] `docs/protocol.md` — тип лога
|
||||||
|
|
||||||
## Тесты, без которых фаза не закрыта
|
## Тесты, без которых фаза не закрыта
|
||||||
|
|
||||||
- [ ] Учебник математики в сумке → полный рост (при учителе на месте)
|
- [x] Учебник математики в сумке → полный рост (при учителе на месте)
|
||||||
- [ ] Тот же учебник в шкафчике или дома → рост как с коэффициентом 0.5
|
- [x] Тот же учебник в шкафчике или дома → рост как с коэффициентом 0.5
|
||||||
- [ ] Учебник другого предмета в сумке не закрывает этот урок
|
- [x] Учебник другого предмета в сумке не закрывает этот урок
|
||||||
- [ ] Одна строка лога за сутки на предмет, не по числу тиков
|
- [x] Одна строка лога за сутки на предмет, не по числу тиков
|
||||||
- [ ] Пак без поля — 0.5, каталог не падает
|
- [x] Пак без поля — 0.5, каталог не падает
|
||||||
|
|
||||||
## Критерий готовности
|
## Критерий готовности
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -417,7 +417,8 @@ 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),
|
`lesson-no-teacher` (the assigned teacher was not standing in the lesson room),
|
||||||
`lesson-cold` (warmth below the behaviour threshold during a lesson that otherwise taught).
|
`lesson-cold` (warmth below the behaviour threshold during a lesson that otherwise taught),
|
||||||
|
`lesson-no-textbook` (the bag had no textbook for that lesson; locker and home do not count).
|
||||||
`thingDef` is the action, apparel or subject def the caption was built from.
|
`thingDef` is the action, apparel or subject def the caption was built from.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using HSchool.Content;
|
using HSchool.Content;
|
||||||
|
using HSchool.People;
|
||||||
|
|
||||||
namespace HSchool.Ai;
|
namespace HSchool.Ai;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much a lesson adds to one skill this step. Hungry or cold learns worse; trait offsets and
|
/// How much a lesson adds to one skill this step. Hungry or cold learns worse; trait offsets and
|
||||||
/// the teacher's subject skill scale the rate. Missing warmth is treated as 1.
|
/// the teacher's subject skill scale the rate. Missing warmth is treated as 1. A missing textbook
|
||||||
|
/// uses <see cref="BehaviorDef.LessonNoTextbookFactor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class LessonLearning
|
public static class LessonLearning
|
||||||
{
|
{
|
||||||
@@ -45,6 +47,28 @@ public static class LessonLearning
|
|||||||
return count == 0 ? 0f : total / count;
|
return count == 0 ? 0f : total / count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when a bag instance carries this lesson's <see cref="InventoryItem.Subject"/>.
|
||||||
|
/// Locker and home do not count — the pupil is already in class.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasTextbookInBag(IReadOnlyList<InventoryItem> items, string subject)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(items);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(subject);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal)
|
||||||
|
&& item.Subject is not null
|
||||||
|
&& item.Subject.Equals(subject, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static float Gain(
|
public static float Gain(
|
||||||
float current,
|
float current,
|
||||||
SkillDef skill,
|
SkillDef skill,
|
||||||
@@ -54,7 +78,8 @@ public static class LessonLearning
|
|||||||
float hunger,
|
float hunger,
|
||||||
int traitOffset,
|
int traitOffset,
|
||||||
float teacherSkill,
|
float teacherSkill,
|
||||||
float warmth = 1f)
|
float warmth = 1f,
|
||||||
|
float textbookFactor = 1f)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(skill);
|
ArgumentNullException.ThrowIfNull(skill);
|
||||||
var delta = share
|
var delta = share
|
||||||
@@ -63,7 +88,8 @@ public static class LessonLearning
|
|||||||
* NeedFactor(hunger)
|
* NeedFactor(hunger)
|
||||||
* NeedFactor(warmth)
|
* NeedFactor(warmth)
|
||||||
* TraitFactor(traitOffset)
|
* TraitFactor(traitOffset)
|
||||||
* TeacherFactor(teacherSkill);
|
* TeacherFactor(teacherSkill)
|
||||||
|
* textbookFactor;
|
||||||
return Math.Clamp(current + delta, skill.Range.Min, skill.Range.Max);
|
return Math.Clamp(current + delta, skill.Range.Min, skill.Range.Max);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -506,6 +506,11 @@ internal static class PeopleDefValidator
|
|||||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonSkillPerHour cannot be negative.");
|
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonSkillPerHour cannot be negative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (behavior.LessonNoTextbookFactor is < 0f or > 1f)
|
||||||
|
{
|
||||||
|
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonNoTextbookFactor must be 0–1.");
|
||||||
|
}
|
||||||
|
|
||||||
if (behavior.SwitchMargin < 0f)
|
if (behavior.SwitchMargin < 0f)
|
||||||
{
|
{
|
||||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' switchMargin cannot be negative.");
|
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' switchMargin cannot be negative.");
|
||||||
|
|||||||
@@ -274,6 +274,12 @@ public sealed class BehaviorDef : Def
|
|||||||
/// <summary>Skill points a lesson adds per game hour, before traits and need state.</summary>
|
/// <summary>Skill points a lesson adds per game hour, before traits and need state.</summary>
|
||||||
public float LessonSkillPerHour { get; init; }
|
public float LessonSkillPerHour { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplier when the bag has no textbook for this lesson. Locker and home do not count.
|
||||||
|
/// A pack without the field keeps vanilla half-gain so the catalog still loads.
|
||||||
|
/// </summary>
|
||||||
|
public float LessonNoTextbookFactor { get; init; } = 0.5f;
|
||||||
|
|
||||||
/// <summary>Inclusive range of extra commute minutes rolled per person per day.</summary>
|
/// <summary>Inclusive range of extra commute minutes rolled per person per day.</summary>
|
||||||
public int CommuteSlackMin { get; init; }
|
public int CommuteSlackMin { get; init; }
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"needThreshold": 0.35,
|
"needThreshold": 0.35,
|
||||||
// Skill points a lesson adds per game hour, before traits and need state.
|
// Skill points a lesson adds per game hour, before traits and need state.
|
||||||
"lessonSkillPerHour": 0.05,
|
"lessonSkillPerHour": 0.05,
|
||||||
|
// No textbook for this lesson in the bag (locker and home do not count).
|
||||||
|
"lessonNoTextbookFactor": 0.5,
|
||||||
// Inclusive extra commute minutes. 0–6 matches the previous hardcoded roll so
|
// Inclusive extra commute minutes. 0–6 matches the previous hardcoded roll so
|
||||||
// the same seed still arrives at the same minute.
|
// the same seed still arrives at the same minute.
|
||||||
"commuteSlackMin": 0,
|
"commuteSlackMin": 0,
|
||||||
|
|||||||
@@ -206,5 +206,6 @@
|
|||||||
"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}",
|
"LessonCold": "too cold in class: {0}",
|
||||||
|
"LessonNoTextbook": "no textbook: {0}",
|
||||||
"core": "Core",
|
"core": "Core",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,5 +206,6 @@
|
|||||||
"ApparelChanged": "переоделся: {0}",
|
"ApparelChanged": "переоделся: {0}",
|
||||||
"LessonNoTeacher": "урок без учителя: {0}",
|
"LessonNoTeacher": "урок без учителя: {0}",
|
||||||
"LessonCold": "замёрз на уроке: {0}",
|
"LessonCold": "замёрз на уроке: {0}",
|
||||||
|
"LessonNoTextbook": "нет учебника: {0}",
|
||||||
"core": "Базовая игра",
|
"core": "Базовая игра",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ namespace HSchool.Simulation;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Grows skills for people who are actually in the lesson: at the room, not walking, not off
|
/// Grows skills for people who are actually in the lesson: at the room, not walking, not off
|
||||||
/// doing something else, and with the assigned teacher standing there. The formula lives in
|
/// doing something else, and with the assigned teacher standing there. A pupil without today's
|
||||||
/// <see cref="HSchool.Ai.LessonLearning"/>.
|
/// textbook in the bag learns at <see cref="BehaviorDef.LessonNoTextbookFactor"/>. The formula
|
||||||
|
/// lives in <see cref="HSchool.Ai.LessonLearning"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class LessonLearningSystem
|
internal static class LessonLearningSystem
|
||||||
{
|
{
|
||||||
@@ -90,6 +91,13 @@ internal static class LessonLearningSystem
|
|||||||
school.TryLogLessonOnce(personId, PersonLogTypes.LessonCold, lesson.Subject);
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonCold, lesson.Subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textbookFactor = 1f;
|
||||||
|
if (person.IsStudent && !LessonLearning.HasTextbookInBag(person.Items, lesson.Subject))
|
||||||
|
{
|
||||||
|
textbookFactor = rules.LessonNoTextbookFactor;
|
||||||
|
school.TryLogLessonOnce(personId, PersonLogTypes.LessonNoTextbook, 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);
|
||||||
@@ -115,7 +123,8 @@ internal static class LessonLearningSystem
|
|||||||
hunger,
|
hunger,
|
||||||
TraitOffset(catalog, traits, share.Skill),
|
TraitOffset(catalog, traits, share.Skill),
|
||||||
teacherSkill,
|
teacherSkill,
|
||||||
warmth);
|
warmth,
|
||||||
|
textbookFactor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public static class PersonLogTypes
|
|||||||
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";
|
public const string LessonCold = "lesson-cold";
|
||||||
|
public const string LessonNoTextbook = "lesson-no-textbook";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,14 +63,17 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|
||||||
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal))
|
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
||||||
|
|| Type.Equals(PersonLogTypes.LessonNoTextbook, 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;
|
||||||
var key = Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
var key = Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
||||||
? "LessonCold"
|
? "LessonCold"
|
||||||
: "LessonNoTeacher";
|
: Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal)
|
||||||
|
? "LessonNoTextbook"
|
||||||
|
: "LessonNoTeacher";
|
||||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using HSchool.Content;
|
using HSchool.Content;
|
||||||
|
using HSchool.People;
|
||||||
|
|
||||||
namespace HSchool.Ai.Tests;
|
namespace HSchool.Ai.Tests;
|
||||||
|
|
||||||
@@ -97,6 +98,40 @@ public class LessonLearningTests
|
|||||||
|
|
||||||
Assert.Equal(explicitWarm, implied);
|
Assert.Equal(explicitWarm, implied);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingTextbook_HalvesGain()
|
||||||
|
{
|
||||||
|
var withBook = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 1f, 0, 100, warmth: 1f, textbookFactor: 1f);
|
||||||
|
var without = LessonLearning.Gain(50, Math, 1, 0.05f, 1f, 1f, 0, 100, warmth: 1f, textbookFactor: 0.5f);
|
||||||
|
|
||||||
|
Assert.Equal((withBook - 50) * 0.5f, without - 50, precision: 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextbookInBag_OnlyThatSubjectCounts()
|
||||||
|
{
|
||||||
|
var items = new[]
|
||||||
|
{
|
||||||
|
new InventoryItem("Textbook", null, 1f, ItemLocations.Bag, "Literature"),
|
||||||
|
new InventoryItem("Textbook", null, 1f, ItemLocations.Locker, "Mathematics"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(LessonLearning.HasTextbookInBag(items, "Mathematics"));
|
||||||
|
Assert.True(LessonLearning.HasTextbookInBag(items, "Literature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextbookAtHome_DoesNotCount()
|
||||||
|
{
|
||||||
|
var items = new[]
|
||||||
|
{
|
||||||
|
new InventoryItem("Textbook", null, 1f, ItemLocations.Home, "Mathematics"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(LessonLearning.HasTextbookInBag(items, "Mathematics"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MissingTeacherSkill_UsesRangeMin_AndStillGains()
|
public void MissingTeacherSkill_UsesRangeMin_AndStillGains()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,6 +64,39 @@ public class BehaviorDefTests
|
|||||||
Assert.Contains("carry mass", error.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("carry mass", error.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingLessonNoTextbookFactor_DefaultsToHalf()
|
||||||
|
{
|
||||||
|
var catalog = _loader.Load(
|
||||||
|
[CatalogLoader.CorePackId],
|
||||||
|
[
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"behavior",
|
||||||
|
"rules",
|
||||||
|
"""{ "defName": "Behavior", "needThreshold": 0.35, "lessonSkillPerHour": 0.05, "commuteSlackMin": 0, "commuteSlackMax": 6, "switchMargin": 0.15 }"""),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Assert.NotNull(catalog.BehaviorRules);
|
||||||
|
Assert.Equal(0.5f, catalog.BehaviorRules.LessonNoTextbookFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LessonNoTextbookFactor_OutOfRange_FailsTheCatalog()
|
||||||
|
{
|
||||||
|
var error = Assert.Throws<ContentLoadException>(() => _loader.Load(
|
||||||
|
[CatalogLoader.CorePackId],
|
||||||
|
[
|
||||||
|
PackDocuments.Def(
|
||||||
|
CatalogLoader.CorePackId,
|
||||||
|
"behavior",
|
||||||
|
"rules",
|
||||||
|
"""{ "defName": "Behavior", "lessonNoTextbookFactor": 1.5 }"""),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Assert.Contains("lessonNoTextbookFactor", error.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NegativeApparelWear_FailsTheCatalog()
|
public void NegativeApparelWear_FailsTheCatalog()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,6 +238,96 @@ public class LessonTeacherPresentTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextbookInBag_GainsFull()
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, homeroom);
|
||||||
|
PlaceTextbook(school, pupilId, "Mathematics", ItemLocations.Bag);
|
||||||
|
var before = SkillOf(school, pupilId, "Mathematics");
|
||||||
|
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
|
||||||
|
Assert.True(SkillOf(school, pupilId, "Mathematics") > before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ItemLocations.Locker)]
|
||||||
|
[InlineData(ItemLocations.Home)]
|
||||||
|
public void TextbookNotInBag_GainsHalf(string location)
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, homeroom);
|
||||||
|
PlaceTextbook(school, pupilId, "Mathematics", ItemLocations.Bag);
|
||||||
|
var start = SkillOf(school, pupilId, "Mathematics");
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
var bagGain = SkillOf(school, pupilId, "Mathematics") - start;
|
||||||
|
|
||||||
|
SetSkill(school, pupilId, "Mathematics", start);
|
||||||
|
PlaceTextbook(school, pupilId, "Mathematics", location);
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
var awayGain = SkillOf(school, pupilId, "Mathematics") - start;
|
||||||
|
|
||||||
|
Assert.True(bagGain > 0);
|
||||||
|
Assert.Equal(bagGain * 0.5f, awayGain, precision: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OtherSubjectInBag_DoesNotCoverThisLesson()
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, homeroom);
|
||||||
|
PlaceTextbook(school, pupilId, "Mathematics", ItemLocations.Bag);
|
||||||
|
var start = SkillOf(school, pupilId, "Mathematics");
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
var mathGain = SkillOf(school, pupilId, "Mathematics") - start;
|
||||||
|
|
||||||
|
SetSkill(school, pupilId, "Mathematics", start);
|
||||||
|
PlaceTextbook(school, pupilId, "Literature", ItemLocations.Bag);
|
||||||
|
LessonLearningSystem.Apply(school, 45);
|
||||||
|
var otherGain = SkillOf(school, pupilId, "Mathematics") - start;
|
||||||
|
|
||||||
|
Assert.Equal(mathGain * 0.5f, otherGain, precision: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoTextbook_LogsOncePerSubject()
|
||||||
|
{
|
||||||
|
var (school, homeroom, pupilId, teacherId) = StaffedMath();
|
||||||
|
using (school)
|
||||||
|
{
|
||||||
|
AdvanceTo(school, LessonStart);
|
||||||
|
SetPlace(school, pupilId, homeroom);
|
||||||
|
SetPlace(school, teacherId, homeroom);
|
||||||
|
PlaceTextbook(school, pupilId, "Mathematics", ItemLocations.Home);
|
||||||
|
|
||||||
|
LessonLearningSystem.Apply(school, 5);
|
||||||
|
LessonLearningSystem.Apply(school, 5);
|
||||||
|
|
||||||
|
var rows = school.DayLog
|
||||||
|
.Where(row => row.PersonId == pupilId && row.Type == PersonLogTypes.LessonNoTextbook)
|
||||||
|
.ToArray();
|
||||||
|
Assert.Single(rows);
|
||||||
|
Assert.Equal("Mathematics", rows[0].ThingDef);
|
||||||
|
Assert.Equal("нет учебника: Математика", rows[0].Caption(school.Catalog!, "ru"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -263,6 +353,25 @@ public class LessonTeacherPresentTests
|
|||||||
return (school, schoolClass.RoomId, pupil.Id, hiredId);
|
return (school, schoolClass.RoomId, pupil.Id, hiredId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void PlaceTextbook(School school, string personId, string subject, string location)
|
||||||
|
{
|
||||||
|
var person = school.Roster!.People.First(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||||
|
if (person.Items is not IList<InventoryItem> items || items.IsReadOnly)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cannot mutate items for {personId}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = items.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (items[i].Subject is not null)
|
||||||
|
{
|
||||||
|
items.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new InventoryItem("Textbook", Color: null, Condition: 1f, location, subject));
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user