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:
Leonid Pershin
2026-08-20 09:27:46 +03:00
co-authored by Cursor
parent f4a793aca4
commit 1b3479f984
13 changed files with 252 additions and 20 deletions
@@ -1,4 +1,5 @@
using HSchool.Content;
using HSchool.People;
namespace HSchool.Ai.Tests;
@@ -97,6 +98,40 @@ public class LessonLearningTests
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]
public void MissingTeacherSkill_UsesRangeMin_AndStillGains()
{
@@ -64,6 +64,39 @@ public class BehaviorDefTests
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]
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(
string? teacherId = null)
{
@@ -263,6 +353,25 @@ public class LessonTeacherPresentTests
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)
{
while (school.Clock.Time < until)