using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Ai; /// When this person appears at the yard and when they start walking home today. public readonly record struct DayPlan(DateOnly Day, DateTime? AppearAt, DateTime? WalkHomeAt, string? FirstRoom) { public bool Comes => AppearAt is not null; } /// /// Day plans. Rain and snow arrive as already-counted extra minutes — this project does not /// know precipitation. /// public static class DayPlans { public static DayPlan Build( DefCatalog catalog, WalkGraph walks, Person person, SchoolClass? schoolClass, Timetable? timetable, DateTime time, int weekDays, int schoolSeed, int extraCommuteMinutes = 0) { ArgumentNullException.ThrowIfNull(catalog); ArgumentNullException.ThrowIfNull(walks); ArgumentNullException.ThrowIfNull(person); var utc = DateTime.SpecifyKind(time, DateTimeKind.Utc); var day = DateOnly.FromDateTime(utc); if (!Duty.ComesToday(person, timetable, catalog, utc, weekDays) || catalog.DayFrame is null) { return new DayPlan(day, null, null, null); } var weekday = SchoolDay.WeekdayIndex(utc); var firstRoom = FirstRoom(person, schoolClass, timetable, catalog, weekday); if (firstRoom is null) { return new DayPlan(day, null, null, null); } var firstStart = FirstStart(person, schoolClass, timetable, catalog, weekday); var lastEnd = LastEnd(person, schoolClass, timetable, catalog, weekday); var travel = walks.Minutes(walks.TerritoryId, firstRoom); if (float.IsInfinity(travel)) { travel = 0f; } var slack = SlackMinutes(catalog, person, schoolSeed, day) + Math.Max(0, extraCommuteMinutes); var appear = DateTime.SpecifyKind(utc.Date.Add(firstStart.ToTimeSpan()).AddMinutes(-(travel + slack)), DateTimeKind.Utc); var walkHome = DateTime.SpecifyKind(utc.Date.Add(lastEnd.ToTimeSpan()), DateTimeKind.Utc); return new DayPlan(day, appear, walkHome, firstRoom); } private static string? FirstRoom( Person person, SchoolClass? schoolClass, Timetable? timetable, DefCatalog catalog, int weekday) { if (Duty.IsOtherStaff(person)) { return person.WorkplaceRoomId; } var lessons = Duty.LessonsToday(person, schoolClass, timetable, weekday); return lessons.Count > 0 ? lessons[0].RoomId : schoolClass?.RoomId; } private static TimeOnly FirstStart( Person person, SchoolClass? schoolClass, Timetable? timetable, DefCatalog catalog, int weekday) { var frame = catalog.DayFrame!; if (Duty.IsOtherStaff(person)) { return SchoolDay.PeriodStart(frame, 1); } var lessons = Duty.LessonsToday(person, schoolClass, timetable, weekday); return lessons.Count > 0 ? SchoolDay.PeriodStart(frame, lessons[0].Period) : SchoolDay.PeriodStart(frame, 1); } private static TimeOnly LastEnd( Person person, SchoolClass? schoolClass, Timetable? timetable, DefCatalog catalog, int weekday) { var frame = catalog.DayFrame!; if (Duty.IsOtherStaff(person)) { return SchoolDay.PeriodEnd(frame, frame.LessonCount); } var lessons = Duty.LessonsToday(person, schoolClass, timetable, weekday); return lessons.Count > 0 ? SchoolDay.PeriodEnd(frame, lessons[^1].Period) : SchoolDay.PeriodEnd(frame, frame.LessonCount); } private static int SlackMinutes(DefCatalog catalog, Person person, int schoolSeed, DateOnly day) { var rng = new Random(Seed.Mix(schoolSeed, person.Id, day.DayNumber, Seed.CommuteSalt)); var min = 0; var max = 6; if (catalog.BehaviorRules is { } rules) { min = rules.CommuteSlackMin; max = rules.CommuteSlackMax; } var extra = rng.Next(min, max + 1); foreach (var name in person.Traits) { if (catalog.Traits.TryGetValue(name, out var trait)) { extra += trait.CommuteMinutes; } } return extra; } }