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.");
}