Implement applicant pool functionality in school simulation, allowing for the management of job seekers who are not yet part of the school roster. Update the catalog to include staffing definitions and enhance the API to support applicant data retrieval. Revise the school architecture to handle applicant refresh logic and ensure proper integration with existing roster management. Update tests to validate the new applicant functionalities and ensure robustness in handling staffing scenarios.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>One person looking for work at this school, with the hourly rate they named.</summary>
|
||||
public sealed record Applicant(Person Person, float HourlyWageAsk);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applicant> Applicants)
|
||||
{
|
||||
public static ApplicantPool Empty { get; } = new(0, 0, []);
|
||||
|
||||
public static ApplicantPool Create(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf)
|
||||
{
|
||||
var week = ApplicantWeeks.Id(asOf);
|
||||
return Fill(catalog, roster, schoolSeed, nameSetId, asOf, new ApplicantPool(week, 0, []));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks each week from <see cref="Week"/> exclusive to <paramref name="asOf"/> inclusive.
|
||||
/// Same previous list, seed and week always yields the same stays and the same newcomers.
|
||||
/// </summary>
|
||||
public ApplicantPool Advance(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf)
|
||||
{
|
||||
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, nameSetId, asOf, week);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
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 nameSetId,
|
||||
DateTime asOf,
|
||||
int week)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
var rng = new Random(Seed.Mix(schoolSeed, week, Seed.ApplicantSalt));
|
||||
var staying = new List<Applicant>(rules.PoolSize);
|
||||
foreach (var applicant in current.Applicants)
|
||||
{
|
||||
if (rng.NextDouble() < rules.StayChance)
|
||||
{
|
||||
staying.Add(applicant);
|
||||
}
|
||||
}
|
||||
|
||||
return Fill(
|
||||
catalog,
|
||||
roster,
|
||||
schoolSeed,
|
||||
nameSetId,
|
||||
asOf,
|
||||
new ApplicantPool(week, current.NextIndex, staying),
|
||||
rng);
|
||||
}
|
||||
|
||||
private static ApplicantPool Fill(
|
||||
DefCatalog catalog,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf,
|
||||
ApplicantPool current,
|
||||
Random? rng = null)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
{
|
||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
||||
}
|
||||
|
||||
rng ??= new Random(Seed.Mix(schoolSeed, current.Week, Seed.ApplicantSalt));
|
||||
var applicants = new List<Applicant>(rules.PoolSize);
|
||||
var taken = new HashSet<string>(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);
|
||||
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.");
|
||||
}
|
||||
Reference in New Issue
Block a user