Files
h-school/src/HSchool.Ai/DayPlan.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

137 lines
4.4 KiB
C#

using HSchool.Content;
using HSchool.People;
using HSchool.Schedule;
namespace HSchool.Ai;
/// <summary>When this person appears at the yard and when they start walking home today.</summary>
public readonly record struct DayPlan(DateOnly Day, DateTime? AppearAt, DateTime? WalkHomeAt, string? FirstRoom)
{
public bool Comes => AppearAt is not null;
}
/// <summary>
/// Day plans. Rain and snow arrive as already-counted extra minutes — this project does not
/// know precipitation.
/// </summary>
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;
}
}