Enhance AI and simulation components with presence management and routing capabilities
- 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:
@@ -468,6 +468,19 @@ public sealed class CatalogLoader
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' is not a homeroom and cannot set seatThing or defaultSeats.");
|
||||
}
|
||||
|
||||
if (!room.Abstract && room.TravelMinutes <= 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' travelMinutes must be positive.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var territory in catalog.Territories.Values)
|
||||
{
|
||||
if (!territory.Abstract && territory.TravelMinutes <= 0)
|
||||
{
|
||||
throw new ContentLoadException($"TerritoryDef '{territory.DefName}' travelMinutes must be positive.");
|
||||
}
|
||||
}
|
||||
|
||||
PeopleDefValidator.Validate(catalog);
|
||||
|
||||
@@ -77,10 +77,17 @@ public sealed class RoomDef : Def
|
||||
|
||||
/// <summary>Editor default when placing a new homeroom. Vanilla classrooms are 16.</summary>
|
||||
public int DefaultSeats { get; init; }
|
||||
|
||||
/// <summary>Game minutes spent occupying this room when walking through it.</summary>
|
||||
public float TravelMinutes { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BuildingDef : Def;
|
||||
|
||||
public sealed class FloorDef : Def;
|
||||
|
||||
public sealed class TerritoryDef : Def;
|
||||
public sealed class TerritoryDef : Def
|
||||
{
|
||||
/// <summary>Game minutes spent occupying the yard when walking through it.</summary>
|
||||
public float TravelMinutes { get; init; }
|
||||
}
|
||||
|
||||
@@ -167,6 +167,11 @@ public sealed class TraitDef : Def
|
||||
/// Added to the hourly wage ask. Positive means the person wants more at the same skills.
|
||||
/// </summary>
|
||||
public float WageAsk { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra minutes of commute slack. Positive arrives earlier; negative cuts it closer.
|
||||
/// </summary>
|
||||
public int CommuteMinutes { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StaffingDef : Def
|
||||
|
||||
@@ -95,6 +95,113 @@ public static class SchoolDay
|
||||
return stamp >= start || stamp <= end;
|
||||
}
|
||||
|
||||
/// <summary>The work window opens at six — the same hour a new school starts.</summary>
|
||||
public static TimeOnly DayStart { get; } = new(6, 0);
|
||||
|
||||
public static bool IsWorkday(DefCatalog catalog, DateTime time, int weekDays)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
EnsureWeekDays(weekDays);
|
||||
var utc = DateTime.SpecifyKind(time, DateTimeKind.Utc);
|
||||
return IsWeekday(utc, weekDays) && !IsHoliday(catalog, utc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Six in the morning until the last bell of the day frame — not the actual timetable.
|
||||
/// A school with no teachers still has a work window on a workday.
|
||||
/// </summary>
|
||||
public static bool InWorkWindow(DefCatalog catalog, DateTime time, int weekDays)
|
||||
{
|
||||
if (!IsWorkday(catalog, time, weekDays) || !TryLastBell(catalog, out var lastBell))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var clock = DateTime.SpecifyKind(time, DateTimeKind.Utc).TimeOfDay;
|
||||
return clock >= DayStart.ToTimeSpan() && clock < lastBell.ToTimeSpan();
|
||||
}
|
||||
|
||||
public static bool TryLastBell(DefCatalog catalog, out TimeOnly lastBell)
|
||||
{
|
||||
lastBell = default;
|
||||
var frame = catalog.DayFrame;
|
||||
if (frame is null || !TryParseTime(frame.FirstLesson, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lastBell = PeriodEnd(frame, frame.LessonCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static TimeOnly PeriodStart(DayFrameDef frame, int period)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
if (period < 1 || period > frame.LessonCount)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), period, "Period is not on the day frame.");
|
||||
}
|
||||
|
||||
if (!TryParseTime(frame.FirstLesson, out var first))
|
||||
{
|
||||
throw new ArgumentException($"Day frame firstLesson '{frame.FirstLesson}' is not a time.", nameof(frame));
|
||||
}
|
||||
|
||||
var cursor = first.ToTimeSpan();
|
||||
var lesson = TimeSpan.FromMinutes(frame.LessonMinutes);
|
||||
for (var i = 1; i < period; i++)
|
||||
{
|
||||
cursor += lesson;
|
||||
cursor += TimeSpan.FromMinutes(i == frame.LongBreakAfter ? frame.LongBreakMinutes : frame.BreakMinutes);
|
||||
}
|
||||
|
||||
return TimeOnly.FromTimeSpan(cursor);
|
||||
}
|
||||
|
||||
public static TimeOnly PeriodEnd(DayFrameDef frame, int period) =>
|
||||
PeriodStart(frame, period).AddMinutes(frame.LessonMinutes);
|
||||
|
||||
/// <summary>
|
||||
/// The next 6:00 of a workday that is still ahead. Night lands on the same morning;
|
||||
/// evening, weekends and holidays walk forward. <see langword="null"/> when none exists
|
||||
/// within <paramref name="maxDays"/>.
|
||||
/// </summary>
|
||||
public static DateTime? NextWorkMorning(DefCatalog catalog, DateTime time, int weekDays, int maxDays = 400)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
EnsureWeekDays(weekDays);
|
||||
if (maxDays < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDays), maxDays, "Search must look at least one day ahead.");
|
||||
}
|
||||
|
||||
var utc = DateTime.SpecifyKind(time, DateTimeKind.Utc);
|
||||
var day = utc.Date;
|
||||
if (utc.TimeOfDay < DayStart.ToTimeSpan() && IsWorkday(catalog, day, weekDays))
|
||||
{
|
||||
return DateTime.SpecifyKind(day.Add(DayStart.ToTimeSpan()), DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
for (var i = 1; i <= maxDays; i++)
|
||||
{
|
||||
var candidate = day.AddDays(i);
|
||||
if (IsWorkday(catalog, candidate, weekDays))
|
||||
{
|
||||
return DateTime.SpecifyKind(candidate.Add(DayStart.ToTimeSpan()), DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void EnsureWeekDays(int weekDays)
|
||||
{
|
||||
if (weekDays is < 5 or > 7)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(weekDays), weekDays, "School week must be 5–7 days.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWeekday(DateTime time, int weekDays)
|
||||
{
|
||||
// Monday = 0 … Sunday = 6. A 5-day week is Mon–Fri; 6 adds Saturday; 7 is every day.
|
||||
|
||||
Reference in New Issue
Block a user