Files
h-school/tests/HSchool.Ai.Tests/WalkingTests.cs
T
Leonid PershinandCursor ce71a91a67 Add rain and snow minutes to the morning commute.
Day plans take a precomputed extra; Ai does not know precipitation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 09:40:24 +03:00

237 lines
10 KiB
C#

using HSchool.Content;
using HSchool.People;
using HSchool.Schedule;
namespace HSchool.Ai.Tests;
public class WalkingTests
{
private static readonly DateTime TuesdayLesson = new(2012, 4, 3, 8, 45, 0, DateTimeKind.Utc);
private static readonly DateTime Sunday = new(2012, 4, 8, 10, 0, 0, DateTimeKind.Utc);
private static readonly DateTime SpringBreak = new(2012, 3, 31, 10, 0, 0, DateTimeKind.Utc);
[Fact]
public void PathFrom201ToFirstFloorRestroom_GoesThroughStairsAndCorridors()
{
var (catalog, map) = Fixtures.Vanilla();
var walks = WalkGraph.Build(catalog, map);
var hops = walks.Path("classroom-201", "restroom-1");
Assert.Equal(["corridor-2", "stairs-2", "stairs-1", "corridor-1", "restroom-1"], hops);
Assert.DoesNotContain("yard", hops);
Assert.Equal(6.5f, walks.Minutes("classroom-201", "restroom-1"));
}
[Fact]
public void PresenceStepper_TakesTheSameMinutesAsTheGraph()
{
var (catalog, map) = Fixtures.Vanilla();
var walks = WalkGraph.Build(catalog, map);
var hops = walks.Path("classroom-201", "restroom-1");
var cost = walks.Minutes("classroom-201", "restroom-1");
var presence = new Presence("classroom-201", 0f, "restroom-1", HeadingHome: false, [.. hops]);
var walked = PresenceStepper.Advance(presence, walks, cost);
Assert.Equal("restroom-1", walked.NodeId);
Assert.Empty(walked.Path);
Assert.Equal(0f, walked.RemainingMinutes);
var stepwise = presence;
var elapsed = 0f;
const float step = 0.25f;
while (stepwise.NodeId != "restroom-1" || stepwise.Path.Length > 0 || stepwise.RemainingMinutes > 0)
{
stepwise = PresenceStepper.Advance(stepwise, walks, step);
elapsed += step;
Assert.True(elapsed <= cost + step);
}
Assert.Equal(cost, elapsed);
}
[Fact]
public void GymAfterClassroom101_CostsSixMinutesAgainstATenMinuteBreak()
{
var (catalog, map) = Fixtures.Vanilla();
var walks = WalkGraph.Build(catalog, map);
var hops = walks.Path("classroom-101", "gym-hall");
Assert.Equal(["corridor-1", "porch", "yard", "gym-hall"], hops);
Assert.Equal(6f, walks.Minutes("classroom-101", "gym-hall"));
Assert.Equal(10, catalog.DayFrame!.BreakMinutes);
Assert.True(walks.Minutes("classroom-101", "gym-hall") < catalog.DayFrame.BreakMinutes);
}
[Fact]
public void ComesToday_IsFalseOnSundayAndHolidays()
{
var (catalog, map) = Fixtures.Vanilla();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayLesson);
var pupil = roster.People.First(person => person.IsStudent);
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
var table = new Timetable(
[new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1)],
[]);
Assert.True(Duty.ComesToday(pupil, table, catalog, TuesdayLesson, weekDays: 5));
Assert.False(Duty.ComesToday(pupil, table, catalog, Sunday, weekDays: 5));
Assert.False(Duty.ComesToday(pupil, table, catalog, SpringBreak, weekDays: 5));
Assert.False(Duty.ComesToday(
roster.People.First(person => person.IsParent && !person.IsStaff && !person.IsStudent),
table,
catalog,
TuesdayLesson,
weekDays: 5));
}
[Fact]
public void ClassWithoutLessons_StaysAway_TeacherWithAnotherClassComes()
{
var (catalog, map) = Fixtures.Vanilla();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayLesson);
var idle = roster.Classes[0];
var busy = roster.Classes[1];
var idlePupil = roster.People.First(person => person.Id == idle.PupilIds[0]);
var busyPupil = roster.People.First(person => person.Id == busy.PupilIds[0]);
var teacher = roster.People.First(person => person.IsParent && !person.IsStudent && !person.IsStaff) with
{
IsStaff = true,
Position = Staffing.TeacherPosition,
};
var table = new Timetable(
[new LessonPlacement(busy.Id, "Mathematics", teacher.Id, busy.RoomId, Day: 1, Period: 1)],
[]);
Assert.False(Duty.ComesToday(idlePupil, table, catalog, TuesdayLesson, weekDays: 5));
Assert.True(Duty.ComesToday(busyPupil, table, catalog, TuesdayLesson, weekDays: 5));
Assert.True(Duty.ComesToday(teacher, table, catalog, TuesdayLesson, weekDays: 5));
Assert.Null(Duty.RoomAt(idlePupil, idle, table, catalog, TuesdayLesson, weekDays: 5));
Assert.Equal(busy.RoomId, Duty.RoomAt(busyPupil, busy, table, catalog, TuesdayLesson, weekDays: 5));
Assert.Equal(busy.RoomId, Duty.RoomAt(teacher, null, table, catalog, TuesdayLesson, weekDays: 5));
}
[Fact]
public void SameSeedAndMap_YieldTheSameDayPlan()
{
var (catalog, map) = Fixtures.Vanilla();
var walks = WalkGraph.Build(catalog, map);
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", TuesdayLesson);
var pupil = roster.People.First(person => person.IsStudent);
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
var table = new Timetable(
[new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1)],
[]);
var first = DayPlans.Build(catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9);
var second = DayPlans.Build(catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9);
Assert.Equal(first, second);
Assert.True(first.Comes);
Assert.Equal(schoolClass.RoomId, first.FirstRoom);
Assert.True(first.AppearAt < DateTime.SpecifyKind(TuesdayLesson.Date.Add(new TimeSpan(8, 30, 0)), DateTimeKind.Utc));
}
[Fact]
public void SnowMinutes_AppearEarlierThanClear()
{
var (clear, snow, extra) = PlansWithExtra(catalog => catalog.BehaviorRules!.CommuteSnowMinutes);
Assert.Equal(6, extra);
Assert.Equal(clear.AppearAt!.Value.AddMinutes(-extra), snow.AppearAt);
}
[Fact]
public void RainMinutes_AppearEarlierThanClear()
{
var (clear, rain, extra) = PlansWithExtra(catalog => catalog.BehaviorRules!.CommuteRainMinutes);
Assert.Equal(3, extra);
Assert.Equal(clear.AppearAt!.Value.AddMinutes(-extra), rain.AppearAt);
}
[Fact]
public void Clear_MatchesOmittedExtraArgument()
{
var (catalog, map) = Fixtures.Vanilla();
var (walks, pupil, schoolClass, table) = ComingPupil(catalog, map);
var implied = DayPlans.Build(catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9);
var explicitZero = DayPlans.Build(
catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extraCommuteMinutes: 0);
Assert.Equal(implied, explicitZero);
Assert.True(implied.Comes);
}
[Fact]
public void SameSeedAndSnowMinutes_YieldTheSameAppearAt()
{
var (catalog, map) = Fixtures.Vanilla();
var (walks, pupil, schoolClass, table) = ComingPupil(catalog, map);
var extra = catalog.BehaviorRules!.CommuteSnowMinutes;
var first = DayPlans.Build(
catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extra);
var second = DayPlans.Build(
catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extra);
Assert.Equal(first.AppearAt, second.AppearAt);
Assert.True(first.Comes);
}
[Fact]
public void Sources_DoNotMentionPrecipitation()
{
var root = Path.Combine(Fixtures.RepoRoot(), "src", "HSchool.Ai");
foreach (var path in Directory.EnumerateFiles(root, "*.cs"))
{
var text = File.ReadAllText(path);
Assert.DoesNotContain("Precipitation", text, StringComparison.Ordinal);
}
}
[Fact]
public void Assembly_DoesNotReferenceArchAspNetOrSockets()
{
var names = typeof(WalkGraph).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
Assert.DoesNotContain(names, name => name.StartsWith("Arch", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Sources_DoNotUseWallClock()
{
var root = Path.Combine(Fixtures.RepoRoot(), "src", "HSchool.Ai");
foreach (var path in Directory.EnumerateFiles(root, "*.cs"))
{
var text = File.ReadAllText(path);
Assert.DoesNotContain("DateTime.Now", text, StringComparison.Ordinal);
Assert.DoesNotContain("DateTime.UtcNow", text, StringComparison.Ordinal);
}
}
private static (DayPlan Clear, DayPlan Weather, int Extra) PlansWithExtra(Func<DefCatalog, int> extraOf)
{
var (catalog, map) = Fixtures.Vanilla();
var (walks, pupil, schoolClass, table) = ComingPupil(catalog, map);
var extra = extraOf(catalog);
var clear = DayPlans.Build(catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9);
var weather = DayPlans.Build(
catalog, walks, pupil, schoolClass, table, TuesdayLesson, weekDays: 5, schoolSeed: 9, extra);
return (clear, weather, extra);
}
private static (WalkGraph Walks, Person Pupil, SchoolClass SchoolClass, Timetable Table) ComingPupil(
DefCatalog catalog,
MapLayout map)
{
var walks = WalkGraph.Build(catalog, map);
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", TuesdayLesson);
var pupil = roster.People.First(person => person.IsStudent);
var schoolClass = roster.Classes.First(row => row.Id == pupil.ClassId);
var table = new Timetable(
[new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1)],
[]);
return (walks, pupil, schoolClass, table);
}
}