namespace HSchool.People; /// One person looking for work at this school, with the hourly rate they named. public sealed record Applicant(Person Person, float HourlyWageAsk); /// /// School-wide applicant list. Not the roster: a new candidate lives only here until hired. /// A parent already in the roster may appear with the same id — that is the same person. /// public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList Applicants) { public static ApplicantPool Empty { get; } = new(0, 0, []); public static ApplicantPool Create( DefCatalog catalog, Roster roster, int schoolSeed, string countryId, DateTime asOf, string? nativeLanguage = null) { var week = ApplicantWeeks.Id(asOf); return Fill(catalog, roster, schoolSeed, countryId, asOf, new ApplicantPool(week, 0, []), nativeLanguage: nativeLanguage); } /// /// Walks each week from exclusive to inclusive. /// Same previous list, seed and week always yields the same stays and the same newcomers. /// public ApplicantPool Advance( DefCatalog catalog, Roster roster, int schoolSeed, string countryId, DateTime asOf, string? nativeLanguage = null) { var target = ApplicantWeeks.Id(asOf); if (target <= Week) { return this; } var pool = this; for (var week = Week + 1; week <= target; week++) { pool = Step(pool, catalog, roster, schoolSeed, countryId, asOf, week, nativeLanguage); } return pool; } public ApplicantPool Without(string personId) => this with { Applicants = Applicants .Where(applicant => !applicant.Person.Id.Equals(personId, StringComparison.Ordinal)) .ToArray(), }; public static float HourlyAsk(DefCatalog catalog, Person person) { var rules = catalog.StaffingRules ?? throw new InvalidOperationException("The catalog has no StaffingDef."); var mean = 0d; if (person.Skills.Count > 0) { var total = 0; foreach (var value in person.Skills.Values) { total += value; } mean = total / (double)person.Skills.Count; } var trait = 0f; foreach (var name in person.Traits) { if (catalog.Traits.TryGetValue(name, out var def)) { trait += def.WageAsk; } } var ask = rules.HourlyWageBase + (float)(rules.HourlyWagePerSkill * mean) + trait; return MathF.Round(MathF.Max(ask, 0f), 2); } private static ApplicantPool Step( ApplicantPool current, DefCatalog catalog, Roster roster, int schoolSeed, string countryId, DateTime asOf, int week, string? nativeLanguage) { var rules = RequireRules(catalog); var rng = new Random(Seed.Mix(schoolSeed, week, Seed.ApplicantSalt)); var staying = new List(rules.PoolSize); foreach (var applicant in current.Applicants) { if (rng.NextDouble() < rules.StayChance) { staying.Add(applicant); } } return Fill( catalog, roster, schoolSeed, countryId, asOf, new ApplicantPool(week, current.NextIndex, staying), rng, nativeLanguage); } private static ApplicantPool Fill( DefCatalog catalog, Roster roster, int schoolSeed, string countryId, DateTime asOf, ApplicantPool current, Random? rng = null, string? nativeLanguage = null) { var rules = RequireRules(catalog); if (!catalog.TryGetCountryNames(countryId, out var names)) { throw new ArgumentException($"Unknown country '{countryId}'.", nameof(countryId)); } var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false); rng ??= new Random(Seed.Mix(schoolSeed, current.Week, Seed.ApplicantSalt)); var applicants = new List(rules.PoolSize); var taken = new HashSet(StringComparer.Ordinal); foreach (var applicant in current.Applicants) { if (applicants.Count >= rules.PoolSize || !taken.Add(applicant.Person.Id)) { continue; } applicants.Add(applicant); } var eligible = roster.People .Where(person => person.IsParent && !person.IsStaff && !taken.Contains(person.Id)) .OrderBy(person => person.Id, StringComparer.Ordinal) .ToList(); var nextIndex = current.NextIndex; while (applicants.Count < rules.PoolSize) { Applicant next; if (eligible.Count > 0 && rng.NextDouble() < rules.ParentChance) { var pick = rng.Next(eligible.Count); var parent = eligible[pick]; eligible.RemoveAt(pick); taken.Add(parent.Id); next = new Applicant(parent, HourlyAsk(catalog, parent)); } else { var (_, person) = FamilyFactory.CreateStaffOnly(catalog, names, schoolSeed, nextIndex, asOf, nativeLanguage: native); nextIndex++; taken.Add(person.Id); next = new Applicant(person, HourlyAsk(catalog, person)); } applicants.Add(next); } return new ApplicantPool(current.Week, nextIndex, applicants); } private static StaffingDef RequireRules(DefCatalog catalog) => catalog.StaffingRules ?? throw new InvalidOperationException("The catalog has no StaffingDef."); }