Enhance AI and simulation components with presence management and routing capabilities
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 14s

- Introduced the `HSchool.Ai` project, responsible for routing, day plans, and presence management.
- Updated the `HSchool.Simulation` project to integrate with the new AI functionalities, improving decision-making and presence tracking.
- Added `travelMinutes` to room and territory definitions, ensuring accurate movement calculations within the simulation.
- Enhanced the `School` class to manage presence and implement empty-time skipping functionality.
- Updated documentation to reflect the new AI features and their impact on school simulation.
- Added tests for presence management and routing to ensure robust functionality and reliability.
This commit is contained in:
Leonid Pershin
2026-08-19 16:33:52 +03:00
parent c16c34a83c
commit 4137400621
50 changed files with 1978 additions and 57 deletions
+125
View File
@@ -0,0 +1,125 @@
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;
}
public static class DayPlans
{
private const int ExtraRollMax = 7;
public static DayPlan Build(
DefCatalog catalog,
WalkGraph walks,
Person person,
SchoolClass? schoolClass,
Timetable? timetable,
DateTime time,
int weekDays,
int schoolSeed)
{
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);
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 extra = rng.Next(0, ExtraRollMax);
foreach (var name in person.Traits)
{
if (catalog.Traits.TryGetValue(name, out var trait))
{
extra += trait.CommuteMinutes;
}
}
return extra;
}
}