Day plans take a precomputed extra; Ai does not know precipitation. Co-authored-by: Cursor <cursoragent@cursor.com>
237 lines
10 KiB
C#
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);
|
|
}
|
|
}
|