namespace HSchool.Ai; /// /// One person's place on the graph. is null when they are off campus. /// There is no "on an edge" state — remaining minutes are spent occupying the current node. /// 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; } /// /// 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. /// 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; }