- 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.
80 lines
2.5 KiB
C#
80 lines
2.5 KiB
C#
namespace HSchool.Ai;
|
|
|
|
/// <summary>
|
|
/// One person's place on the graph. <see cref="NodeId"/> is null when they are off campus.
|
|
/// There is no "on an edge" state — remaining minutes are spent occupying the current node.
|
|
/// </summary>
|
|
public readonly record struct Presence(
|
|
string? NodeId,
|
|
float RemainingMinutes,
|
|
string? DestinationId,
|
|
bool HeadingHome,
|
|
string[] Path)
|
|
{
|
|
public static Presence OffCampus { get; } = new(null, 0f, null, false, []);
|
|
|
|
public bool IsOnCampus => NodeId is not null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hot-path movement in the library so tests can prove travel time without a world. Simulation
|
|
/// copies the same loop onto components and does not call this every tick.
|
|
/// </summary>
|
|
public static class PresenceStepper
|
|
{
|
|
public static Presence Advance(Presence presence, WalkGraph walks, float minutes)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(walks);
|
|
if (presence.NodeId is null || minutes <= 0)
|
|
{
|
|
return presence;
|
|
}
|
|
|
|
var remaining = presence.RemainingMinutes - minutes;
|
|
var node = presence.NodeId;
|
|
var path = presence.Path;
|
|
var index = 0;
|
|
while (remaining <= 0 && index < path.Length)
|
|
{
|
|
node = path[index];
|
|
index++;
|
|
remaining += walks.TravelMinutes(node);
|
|
}
|
|
|
|
if (remaining < 0)
|
|
{
|
|
remaining = 0;
|
|
}
|
|
|
|
var leftover = index >= path.Length ? [] : path[index..];
|
|
return presence with { NodeId = node, RemainingMinutes = remaining, Path = leftover };
|
|
}
|
|
|
|
public static Presence StartWalk(Presence presence, WalkGraph walks, string destination, bool headingHome)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(walks);
|
|
if (presence.NodeId is null)
|
|
{
|
|
var path = walks.Path(walks.TerritoryId, destination);
|
|
return new Presence(walks.TerritoryId, 0f, destination, headingHome, [.. path]);
|
|
}
|
|
|
|
var hops = walks.Path(presence.NodeId, destination);
|
|
return presence with
|
|
{
|
|
DestinationId = destination,
|
|
HeadingHome = headingHome,
|
|
Path = [.. hops],
|
|
};
|
|
}
|
|
|
|
public static Presence ArriveOffCampus(Presence presence) =>
|
|
presence.NodeId is not null
|
|
&& presence.HeadingHome
|
|
&& presence.Path.Length == 0
|
|
&& presence.RemainingMinutes <= 0
|
|
&& presence.NodeId.Equals(presence.DestinationId, StringComparison.Ordinal)
|
|
? Presence.OffCampus
|
|
: presence;
|
|
}
|