Files
h-school/tests/HSchool.Simulation.Tests/LessonTeacherPresentTests.cs
T
Leonid PershinandCursor c2579d6767 Scale lesson skill gain by the teacher's subject skill.
Hiring a 78-math applicant already cost more than a 41; the class now actually learns faster when that teacher is in the room.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 08:54:13 +03:00

320 lines
12 KiB
C#

using Arch.Core;
using HSchool.Ai;
using HSchool.Content;
using HSchool.People;
using HSchool.Schedule;
namespace HSchool.Simulation.Tests;
public class LessonTeacherPresentTests
{
private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
private static readonly DateTime LessonStart = new(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc);
[Fact]
public void PupilAndTeacherStanding_GainsSkill()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, homeroom);
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.True(SkillOf(school, pupilId, "Mathematics") > before);
}
}
[Fact]
public void TeacherInAnotherRoom_DoesNotGain()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, "restroom-1");
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.Equal(before, SkillOf(school, pupilId, "Mathematics"));
}
}
[Fact]
public void TeacherWalking_DoesNotGain()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, "corridor-1", path: [homeroom], remaining: 1.5f);
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.Equal(before, SkillOf(school, pupilId, "Mathematics"));
}
}
[Fact]
public void MissingTeacherId_DoesNotGain()
{
var (school, homeroom, pupilId, _) = StaffedMath(teacherId: "nobody");
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.Equal(before, SkillOf(school, pupilId, "Mathematics"));
}
}
[Fact]
public void NoTeacher_LogsOncePerSubject()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, "restroom-1");
LessonLearningSystem.Apply(school, 5);
LessonLearningSystem.Apply(school, 5);
var rows = school.DayLog
.Where(row => row.PersonId == pupilId && row.Type == PersonLogTypes.LessonNoTeacher)
.ToArray();
Assert.Single(rows);
Assert.Equal("Mathematics", rows[0].ThingDef);
Assert.Equal(
"урок без учителя: Математика",
rows[0].Caption(school.Catalog!, "ru"));
}
}
[Fact]
public void HungryStillGainsLess_WhenTeacherIsPresent()
{
var (school, homeroom, firstId, teacherId) = StaffedMath();
using (school)
{
var schoolClass = school.Roster!.Classes.First(row => row.PupilIds.Contains(firstId));
var secondId = schoolClass.PupilIds.First(id => id != firstId);
AdvanceTo(school, LessonStart);
SetPlace(school, firstId, homeroom);
SetPlace(school, secondId, homeroom);
SetPlace(school, teacherId, homeroom);
SetNeed(school, firstId, "Hunger", 0.2f);
SetNeed(school, secondId, "Hunger", 1f);
var hungryBefore = SkillOf(school, firstId, "Mathematics");
var fullBefore = SkillOf(school, secondId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
var hungryGain = SkillOf(school, firstId, "Mathematics") - hungryBefore;
var fullGain = SkillOf(school, secondId, "Mathematics") - fullBefore;
Assert.True(fullGain > 0);
Assert.True(hungryGain > 0);
Assert.True(fullGain > hungryGain);
}
}
[Fact]
public void WeakTeacher_GainsLessThanStrong()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, homeroom);
SetSkill(school, teacherId, "Mathematics", 20);
var beforeWeak = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
var weakGain = SkillOf(school, pupilId, "Mathematics") - beforeWeak;
SetSkill(school, pupilId, "Mathematics", beforeWeak);
SetSkill(school, teacherId, "Mathematics", 80);
var beforeStrong = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
var strongGain = SkillOf(school, pupilId, "Mathematics") - beforeStrong;
Assert.True(weakGain > 0);
Assert.True(strongGain > weakGain);
}
}
[Fact]
public void MissingTeacherSkillKey_StillGains()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, homeroom);
ClearSkill(school, teacherId, "Mathematics");
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.True(SkillOf(school, pupilId, "Mathematics") > before);
}
}
[Fact]
public void TeacherSkill_DoesNotRescueAbsence()
{
var (school, homeroom, pupilId, teacherId) = StaffedMath();
using (school)
{
AdvanceTo(school, LessonStart);
SetPlace(school, pupilId, homeroom);
SetPlace(school, teacherId, "restroom-1");
SetSkill(school, teacherId, "Mathematics", 100);
var before = SkillOf(school, pupilId, "Mathematics");
LessonLearningSystem.Apply(school, 45);
Assert.Equal(before, SkillOf(school, pupilId, "Mathematics"));
}
}
private static (School School, string Homeroom, string PupilId, string TeacherId) StaffedMath(
string? teacherId = null)
{
var (catalog, map) = Vanilla();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning);
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning);
var hired = Staffing.Hire(catalog, map, roster, pool, pool.Applicants[0].Person.Id, Staffing.TeacherPosition, 1_000_000f);
Assert.Equal(StaffingError.None, hired.Error);
roster = hired.Roster;
pool = hired.Pool;
var hiredId = roster.People.First(person => person.IsStaff).Id;
var schoolClass = roster.Classes.First(row =>
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
var school = School.Create(1, "Урок", TuesdayMorning, catalog, map);
school.InstallPeople(roster, seed: 1, "Russia", pool);
school.SetTimetable(new Timetable(
[new LessonPlacement(schoolClass.Id, "Mathematics", teacherId ?? hiredId, schoolClass.RoomId, Day: 1, Period: 1)],
[]));
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
var pupil = schoolClass.PupilIds
.Select(id => school.Roster!.People.First(person => person.Id == id))
.First(person => !person.Traits.Contains("Lazy"));
return (school, schoolClass.RoomId, pupil.Id, hiredId);
}
private static void AdvanceTo(School school, DateTime until)
{
while (school.Clock.Time < until)
{
school.Tick(0.2d, 5d);
}
}
private static void SetPlace(School school, string personId, string node, string[]? path = null, float remaining = 0f)
{
var query = new QueryDescription().WithAll<PersonIdentity, Presence>();
school.World.Query(
in query,
(ref PersonIdentity identity, ref Presence presence) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
presence = new Presence(node, remaining, node, HeadingHome: false, path ?? []);
}
});
}
private static void SetNeed(School school, string personId, string need, float value)
{
var query = new QueryDescription().WithAll<PersonIdentity, PersonNeeds>();
school.World.Query(
in query,
(ref PersonIdentity identity, ref PersonNeeds needs) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
needs.Values[need] = value;
}
});
}
private static float SkillOf(School school, string personId, string skill)
{
var value = float.NaN;
var query = new QueryDescription().WithAll<PersonIdentity, PersonSkills>();
school.World.Query(
in query,
(ref PersonIdentity identity, ref PersonSkills skills) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
value = skills.Values.GetValueOrDefault(skill);
}
});
return value;
}
private static void SetSkill(School school, string personId, string skill, float value)
{
var query = new QueryDescription().WithAll<PersonIdentity, PersonSkills>();
school.World.Query(
in query,
(ref PersonIdentity identity, ref PersonSkills skills) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
skills.Values[skill] = value;
}
});
}
private static void ClearSkill(School school, string personId, string skill)
{
var query = new QueryDescription().WithAll<PersonIdentity, PersonSkills>();
school.World.Query(
in query,
(ref PersonIdentity identity, ref PersonSkills skills) =>
{
if (identity.Id.Equals(personId, StringComparison.Ordinal))
{
skills.Values.Remove(skill);
}
});
}
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
{
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
var documents = new List<ContentDocument>();
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
{
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
}
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
Assert.NotNull(map);
return (catalog, map);
}
}