- Updated `ai.md` to clarify the mechanics of hunger restoration and the importance of lunch breaks in the school schedule. - Revised `schedule.md` to detail the new lunch break structure, allowing for separate sittings for different grade levels. - Enhanced `Decision.cs` and `DecisionPlanner.cs` to incorporate logic for lunch breaks, ensuring that students only leave lessons during their designated lunch windows. - Updated `DayFrameDef` and related classes to support multiple lunch breaks and validate their configurations. - Adjusted tests to validate the new decision-making logic regarding lunch breaks and hunger management, ensuring robust functionality. - Improved localization strings to reflect changes in the school day structure and lunch functionalities.
161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
using HSchool.Content;
|
|
using HSchool.People;
|
|
using HSchool.Schedule;
|
|
|
|
namespace HSchool.Simulation.Tests;
|
|
|
|
/// <summary>
|
|
/// The canteen is fed in two sittings so it is not the whole school at once: juniors eat in the
|
|
/// break after the third lesson, seniors after the fourth. Before that, everybody crossed the
|
|
/// hunger threshold at roughly the same minute and the room could not hold them.
|
|
/// </summary>
|
|
public class LunchTests
|
|
{
|
|
private static readonly DateTime Tuesday6 = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
|
|
|
[Fact]
|
|
public void JuniorsEatAtTheFirstSitting_SeniorsAtTheSecond()
|
|
{
|
|
using var school = StaffedSchool();
|
|
var (junior, senior) = Parallels(school);
|
|
|
|
// Staff belong to no parallel and may eat at either sitting, so the claim is about pupils:
|
|
// the two halves of the school never meet in the canteen.
|
|
AdvanceTo(school, new DateTime(2012, 4, 3, 11, 15, 0, DateTimeKind.Utc));
|
|
var first = InCafeteria(school);
|
|
Assert.Contains(first, junior.Contains);
|
|
Assert.DoesNotContain(first, senior.Contains);
|
|
|
|
AdvanceTo(school, new DateTime(2012, 4, 3, 12, 20, 0, DateTimeKind.Utc));
|
|
var second = InCafeteria(school);
|
|
Assert.Contains(second, senior.Contains);
|
|
Assert.DoesNotContain(second, junior.Contains);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hunger is restored off campus — people eat at home — so a school week does not leave the
|
|
/// roster starving. It used to: nothing refilled hunger except eight chairs, and by the end of
|
|
/// the week a sixth of the school sat at zero.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AfterASchoolDay_NobodyIsLeftStarving()
|
|
{
|
|
using var school = StaffedSchool();
|
|
var pupils = school.Roster!.People.Where(person => person.IsStudent).Select(person => person.Id)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
AdvanceTo(school, new DateTime(2012, 4, 4, 8, 0, 0, DateTimeKind.Utc));
|
|
|
|
var hunger = Hunger(school, pupils);
|
|
Assert.NotEmpty(hunger);
|
|
Assert.All(hunger.Values, value => Assert.True(value > 0.5f, $"somebody came back to school at {value:F2}"));
|
|
}
|
|
|
|
private static School StaffedSchool()
|
|
{
|
|
var (catalog, map) = Vanilla();
|
|
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Tuesday6);
|
|
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Tuesday6);
|
|
|
|
foreach (var subject in catalog.Subjects.Values.Where(def => !def.Abstract).Select(def => def.DefName))
|
|
{
|
|
if (pool.Applicants.Count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var candidate = pool.Applicants[0].Person.Id;
|
|
var hired = Staffing.Hire(catalog, map, roster, pool, candidate, Staffing.TeacherPosition, 1_000_000f);
|
|
if (hired.Error != StaffingError.None)
|
|
{
|
|
break;
|
|
}
|
|
|
|
roster = hired.Roster;
|
|
pool = hired.Pool;
|
|
var assigned = Staffing.AssignSubject(catalog, roster, pool, candidate, subject, 1_000_000f);
|
|
if (assigned.Error == StaffingError.None)
|
|
{
|
|
roster = assigned.Roster;
|
|
}
|
|
}
|
|
|
|
var school = School.Create(1, "Столовая", Tuesday6, catalog, map);
|
|
school.InstallPeople(roster, seed: 1, "Slavic", pool);
|
|
school.SetTimetable(SchoolTimetables.Build(catalog, map, roster, null, weekDays: 5));
|
|
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 64);
|
|
return school;
|
|
}
|
|
|
|
private static (HashSet<string> Junior, HashSet<string> Senior) Parallels(School school)
|
|
{
|
|
var junior = new HashSet<string>(StringComparer.Ordinal);
|
|
var senior = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var row in school.Roster!.Classes)
|
|
{
|
|
foreach (var id in row.PupilIds)
|
|
{
|
|
(row.Year <= 5 ? junior : senior).Add(id);
|
|
}
|
|
}
|
|
|
|
return (junior, senior);
|
|
}
|
|
|
|
private static string[] InCafeteria(School school) =>
|
|
school.CapturePresence()
|
|
.Where(row => row.NodeId == "cafeteria")
|
|
.Select(row => row.PersonId)
|
|
.ToArray();
|
|
|
|
private static Dictionary<string, float> Hunger(School school, HashSet<string> pupils)
|
|
{
|
|
var values = new Dictionary<string, float>(StringComparer.Ordinal);
|
|
var query = new Arch.Core.QueryDescription().WithAll<PersonIdentity, PersonNeeds>();
|
|
school.World.Query(in query, (ref PersonIdentity identity, ref PersonNeeds needs) =>
|
|
{
|
|
if (pupils.Contains(identity.Id) && needs.Values.TryGetValue("Hunger", out var value))
|
|
{
|
|
values[identity.Id] = value;
|
|
}
|
|
});
|
|
return values;
|
|
}
|
|
|
|
private static void AdvanceTo(School school, DateTime until)
|
|
{
|
|
while (school.Clock.Time < until)
|
|
{
|
|
school.Tick(0.2d, 5d);
|
|
}
|
|
}
|
|
|
|
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
|
|
{
|
|
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
|
var documents = PackDocumentsFrom(root);
|
|
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
|
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
|
|
Assert.NotNull(map);
|
|
return (catalog, map);
|
|
}
|
|
|
|
private static IReadOnlyList<ContentDocument> PackDocumentsFrom(string root)
|
|
{
|
|
var documents = new List<ContentDocument>();
|
|
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
|
|
{
|
|
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
|
|
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var relative = Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, (char)47);
|
|
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
|
|
}
|
|
|
|
return documents;
|
|
}
|
|
}
|