namespace HSchool.People; internal readonly record struct FamilyPlan(int FamilyIndex, IReadOnlyList Seats); /// /// Walks seats in order and groups them into families of 1–3, taking a run of consecutive seats /// per family. Callers hand in a shuffled seat list — a run of *adjacent* seats is one classroom, /// which would put every sibling in the same class. /// /// A family whose intended size does not fit the remainder takes one seat instead of shrinking, /// so growing the seat list only disturbs the tail. /// internal static class FamilyPlanner { /// /// One child per family. Used by the yearly intake: every seat there is a first-year seat, so /// grouping would hand a family two or three same-age children at once. An arriving pupil who /// does have an older sibling gets attached to that family instead. /// public static IReadOnlyList Singletons(IReadOnlyList seats, int startIndex) { var plans = new FamilyPlan[seats.Count]; for (var i = 0; i < seats.Count; i++) { plans[i] = new FamilyPlan(startIndex + i, [seats[i]]); } return plans; } public static IReadOnlyList Plan(int schoolSeed, IReadOnlyList seats, int startIndex = 0) { var plans = new List(); var offset = 0; var index = startIndex; while (offset < seats.Count) { var remaining = seats.Count - offset; var want = ChildCount(schoolSeed, index); var take = want <= remaining ? want : 1; var slice = new PupilSeat[take]; for (var i = 0; i < take; i++) { slice[i] = seats[offset + i]; } plans.Add(new FamilyPlan(index, slice)); offset += take; index++; } return plans; } public static int ChildCount(int schoolSeed, int familyIndex) { var rng = new Random(Seed.Mix(schoolSeed, familyIndex, Seed.ChildCountSalt)); var roll = rng.Next(100); if (roll < 50) { return 1; } return roll < 85 ? 2 : 3; } }