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:
@@ -1,4 +1,5 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
@@ -83,6 +84,23 @@ public sealed class School : IDisposable
|
||||
/// <summary>True after yearly intake until the worker rebuilds around remaining locks.</summary>
|
||||
public bool TimetableDirty { get; private set; }
|
||||
|
||||
/// <summary>Walk matrix for this map. Null in clock-only tests.</summary>
|
||||
internal WalkGraph? Walks { get; private set; }
|
||||
|
||||
internal int SchoolWeekDays { get; private set; } = 5;
|
||||
|
||||
internal int MaxDecisionsPerTick { get; private set; } = 64;
|
||||
|
||||
internal int MaxSkipDays { get; private set; } = 400;
|
||||
|
||||
internal DateOnly? PlanDay { get; set; }
|
||||
|
||||
internal DaySlot? LastDecisionSlot { get; set; }
|
||||
|
||||
internal Dictionary<string, DayPlan> Plans { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
internal Queue<string> DecisionQueue { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a roster that already matches the map. Spawns entities; does not write to disk.
|
||||
/// </summary>
|
||||
@@ -96,6 +114,61 @@ public sealed class School : IDisposable
|
||||
NameSetId = nameSetId;
|
||||
Applicants = applicants;
|
||||
RosterSpawner.Spawn(World, roster);
|
||||
PlanDay = null;
|
||||
LastDecisionSlot = null;
|
||||
Plans.Clear();
|
||||
DecisionQueue.Clear();
|
||||
}
|
||||
|
||||
public void ConfigurePresence(int weekDays = 5, int maxDecisionsPerTick = 64, int maxSkipDays = 400)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
SchoolWeekDays = weekDays;
|
||||
MaxDecisionsPerTick = maxDecisionsPerTick;
|
||||
MaxSkipDays = maxSkipDays;
|
||||
if (Catalog is not null && Map is not null)
|
||||
{
|
||||
Walks = WalkGraph.Build(Catalog, Map);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<PresenceSnapshot> CapturePresence() => PresenceSystem.Capture(this);
|
||||
|
||||
public void RestorePresence(IReadOnlyList<PresenceSnapshot>? saved) => PresenceSystem.Restore(this, saved);
|
||||
|
||||
public bool IsCampusEmpty() => PresenceSystem.IsEmpty(this);
|
||||
|
||||
public SkipEmptyResult TrySkipEmpty()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (Catalog is null)
|
||||
{
|
||||
return SkipEmptyResult.Fail(SkipEmptyError.NoMorning);
|
||||
}
|
||||
|
||||
if (!IsCampusEmpty())
|
||||
{
|
||||
return SkipEmptyResult.Fail(SkipEmptyError.PeoplePresent);
|
||||
}
|
||||
|
||||
if (SchoolDay.InWorkWindow(Catalog, Clock.Time, SchoolWeekDays))
|
||||
{
|
||||
return SkipEmptyResult.Fail(SkipEmptyError.InWorkWindow);
|
||||
}
|
||||
|
||||
var next = SchoolDay.NextWorkMorning(Catalog, Clock.Time, SchoolWeekDays, MaxSkipDays);
|
||||
if (next is null)
|
||||
{
|
||||
return SkipEmptyResult.Fail(SkipEmptyError.NoMorning);
|
||||
}
|
||||
|
||||
var before = Clock.Time;
|
||||
Clock.JumpTo(next.Value);
|
||||
var peopleChanged = TryYearlyIntake(before, next.Value);
|
||||
peopleChanged |= TryApplicantRefresh();
|
||||
PlanDay = null;
|
||||
LastDecisionSlot = null;
|
||||
return new SkipEmptyResult(SkipEmptyError.None, next.Value, peopleChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,7 +183,9 @@ public sealed class School : IDisposable
|
||||
|
||||
Roster = roster;
|
||||
Applicants = applicants;
|
||||
var snapshot = PresenceSystem.Capture(this);
|
||||
RosterSpawner.Replace(World, roster);
|
||||
PresenceSystem.Restore(this, snapshot);
|
||||
TimetableDirty = true;
|
||||
}
|
||||
|
||||
@@ -120,6 +195,11 @@ public sealed class School : IDisposable
|
||||
ArgumentNullException.ThrowIfNull(timetable);
|
||||
Timetable = timetable;
|
||||
TimetableDirty = false;
|
||||
LastDecisionSlot = null;
|
||||
foreach (var id in Roster?.People.Select(person => person.Id) ?? [])
|
||||
{
|
||||
DecisionQueue.Enqueue(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs one fixed step of the school: calendar, yearly intake, applicant refresh, then need decay.</summary>
|
||||
@@ -135,6 +215,13 @@ public sealed class School : IDisposable
|
||||
{
|
||||
peopleChanged = TryYearlyIntake(before, Clock.Time);
|
||||
peopleChanged |= TryApplicantRefresh();
|
||||
if (peopleChanged)
|
||||
{
|
||||
PlanDay = null;
|
||||
LastDecisionSlot = null;
|
||||
}
|
||||
|
||||
PresenceSystem.Apply(this, gameMinutes);
|
||||
if (Catalog is not null)
|
||||
{
|
||||
NeedDecay.Apply(World, Catalog, gameMinutes);
|
||||
@@ -160,7 +247,9 @@ public sealed class School : IDisposable
|
||||
|
||||
if (changed)
|
||||
{
|
||||
var snapshot = PresenceSystem.Capture(this);
|
||||
RosterSpawner.Replace(World, Roster);
|
||||
PresenceSystem.Restore(this, snapshot);
|
||||
TimetableDirty = true;
|
||||
}
|
||||
|
||||
@@ -197,3 +286,18 @@ public sealed class School : IDisposable
|
||||
Arch.Core.World.Destroy(World);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SkipEmptyError
|
||||
{
|
||||
None,
|
||||
PeoplePresent,
|
||||
InWorkWindow,
|
||||
NoMorning,
|
||||
}
|
||||
|
||||
public readonly record struct SkipEmptyResult(SkipEmptyError Error, DateTime? Time, bool PeopleChanged)
|
||||
{
|
||||
public bool Succeeded => Error == SkipEmptyError.None;
|
||||
|
||||
public static SkipEmptyResult Fail(SkipEmptyError error) => new(error, null, false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user