Enhance protocol and simulation features with activity tracking and behavior definitions

- Updated protocol documentation to include new `activity` and `activityLabel` fields in the person card response, reflecting real-time activity status.
- Introduced `BehaviorDef` to define behavior rules, including need thresholds and lesson skill gains, enhancing AI decision-making.
- Revised the `DefCatalog` to incorporate behavior definitions and updated validation logic to ensure proper behavior handling.
- Enhanced the simulation to manage presence and activity states, allowing for more dynamic interactions within the school environment.
- Updated tests to validate the new activity tracking and behavior functionalities, ensuring robust performance and reliability.
- Improved localization strings to support new activity and behavior features, enhancing user experience.
This commit is contained in:
Leonid Pershin
2026-08-19 19:49:44 +03:00
parent 3c54f981b7
commit b135a9caad
42 changed files with 1134 additions and 59 deletions
+53
View File
@@ -0,0 +1,53 @@
using HSchool.Content;
namespace HSchool.Ai;
/// <summary>One in-progress action. <see cref="ActionId"/> is null when the person is idle.</summary>
public readonly record struct ActivityProgress(string? ActionId, string? Thing, float RemainingMinutes)
{
public static ActivityProgress Idle { get; } = new(null, null, 0f);
public bool IsActive => ActionId is not null;
}
/// <summary>
/// Start, countdown and refill for one action. Occupancy is a count check — two people cannot
/// take the last chair. Simulation copies this onto components; the library has no world.
/// </summary>
public static class ActionStepper
{
public static ActivityProgress Begin(ActionDef def)
{
ArgumentNullException.ThrowIfNull(def);
var thing = string.IsNullOrWhiteSpace(def.Thing) ? null : def.Thing;
return new ActivityProgress(def.DefName, thing, def.Minutes);
}
public static ActivityProgress Advance(ActivityProgress activity, float minutes, out bool completed)
{
if (!activity.IsActive)
{
completed = false;
return activity;
}
var left = activity.RemainingMinutes - minutes;
if (left <= 0f)
{
completed = true;
return ActivityProgress.Idle;
}
completed = false;
return activity with { RemainingMinutes = left };
}
public static bool CanOccupy(int available, int occupied) => occupied < available;
public static float ApplyNeedGain(float current, ActionDef action, NeedDef need)
{
ArgumentNullException.ThrowIfNull(action);
ArgumentNullException.ThrowIfNull(need);
return Math.Clamp(current + action.NeedGain, need.Min, need.Max);
}
}
+9 -3
View File
@@ -12,8 +12,6 @@ public readonly record struct DayPlan(DateOnly Day, DateTime? AppearAt, DateTime
public static class DayPlans
{
private const int ExtraRollMax = 7;
public static DayPlan Build(
DefCatalog catalog,
WalkGraph walks,
@@ -111,7 +109,15 @@ public static class DayPlans
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);
var min = 0;
var max = 6;
if (catalog.BehaviorRules is { } rules)
{
min = rules.CommuteSlackMin;
max = rules.CommuteSlackMax;
}
var extra = rng.Next(min, max + 1);
foreach (var name in person.Traits)
{
if (catalog.Traits.TryGetValue(name, out var trait))