Enhance school creation and staffing management with native language support
- Updated protocol documentation to include `nativeLanguages` in `nameSets` and added `nativeLanguage` to school creation options. - Enhanced the UI for school creation to allow selection of native languages, improving user experience. - Revised API interfaces to accommodate new native language features, ensuring proper data handling. - Improved localization strings to support new native language functionalities in both English and Russian. - Updated tests to validate the new native language features and ensure robust functionality in staffing scenarios.
This commit is contained in:
@@ -16,10 +16,11 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf)
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var week = ApplicantWeeks.Id(asOf);
|
||||
return Fill(catalog, roster, schoolSeed, nameSetId, asOf, new ApplicantPool(week, 0, []));
|
||||
return Fill(catalog, roster, schoolSeed, nameSetId, asOf, new ApplicantPool(week, 0, []), nativeLanguage: nativeLanguage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,7 +32,8 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf)
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var target = ApplicantWeeks.Id(asOf);
|
||||
if (target <= Week)
|
||||
@@ -42,7 +44,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
var pool = this;
|
||||
for (var week = Week + 1; week <= target; week++)
|
||||
{
|
||||
pool = Step(pool, catalog, roster, schoolSeed, nameSetId, asOf, week);
|
||||
pool = Step(pool, catalog, roster, schoolSeed, nameSetId, asOf, week, nativeLanguage);
|
||||
}
|
||||
|
||||
return pool;
|
||||
@@ -93,7 +95,8 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf,
|
||||
int week)
|
||||
int week,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
var rng = new Random(Seed.Mix(schoolSeed, week, Seed.ApplicantSalt));
|
||||
@@ -113,7 +116,8 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
nameSetId,
|
||||
asOf,
|
||||
new ApplicantPool(week, current.NextIndex, staying),
|
||||
rng);
|
||||
rng,
|
||||
nativeLanguage);
|
||||
}
|
||||
|
||||
private static ApplicantPool Fill(
|
||||
@@ -123,7 +127,8 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
string nameSetId,
|
||||
DateTime asOf,
|
||||
ApplicantPool current,
|
||||
Random? rng = null)
|
||||
Random? rng = null,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var rules = RequireRules(catalog);
|
||||
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
||||
@@ -131,6 +136,8 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
||||
}
|
||||
|
||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
||||
|
||||
rng ??= new Random(Seed.Mix(schoolSeed, current.Week, Seed.ApplicantSalt));
|
||||
var applicants = new List<Applicant>(rules.PoolSize);
|
||||
var taken = new HashSet<string>(StringComparer.Ordinal);
|
||||
@@ -163,7 +170,7 @@ public sealed record ApplicantPool(int Week, int NextIndex, IReadOnlyList<Applic
|
||||
}
|
||||
else
|
||||
{
|
||||
var (_, person) = FamilyFactory.CreateStaffOnly(catalog, names, schoolSeed, nextIndex, asOf);
|
||||
var (_, person) = FamilyFactory.CreateStaffOnly(catalog, names, schoolSeed, nextIndex, asOf, nativeLanguage: native);
|
||||
nextIndex++;
|
||||
taken.Add(person.Id);
|
||||
next = new Applicant(person, HourlyAsk(catalog, person));
|
||||
|
||||
@@ -15,7 +15,8 @@ internal static class FamilyFactory
|
||||
int schoolSeed,
|
||||
FamilyPlan plan,
|
||||
DateTime yearStart,
|
||||
DateTime asOf)
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var rng = new Random(Seed.Mix(schoolSeed, plan.FamilyIndex, Seed.AppearanceSalt));
|
||||
var familyId = $"f{plan.FamilyIndex}";
|
||||
@@ -89,7 +90,8 @@ internal static class FamilyFactory
|
||||
surname,
|
||||
fatherGiven,
|
||||
NameGrammar.Patronymic(fatherPatronymicSource.Form, female: false, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0));
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage));
|
||||
}
|
||||
|
||||
if (hasMother)
|
||||
@@ -108,7 +110,8 @@ internal static class FamilyFactory
|
||||
surname,
|
||||
motherGiven,
|
||||
NameGrammar.Patronymic(motherPatronymicSource.Form, female: true, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0));
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage));
|
||||
}
|
||||
|
||||
var childIds = new List<string>(childDrafts.Length);
|
||||
@@ -127,7 +130,8 @@ internal static class FamilyFactory
|
||||
draft,
|
||||
asOf,
|
||||
surname,
|
||||
fatherGiven.Form));
|
||||
fatherGiven.Form,
|
||||
nativeLanguage));
|
||||
}
|
||||
|
||||
var family = new Family(familyId, parentIds, childIds, childIds.Count, fatherGiven.Form, surname.Male);
|
||||
@@ -146,7 +150,8 @@ internal static class FamilyFactory
|
||||
PupilSeat seat,
|
||||
DateTime yearStart,
|
||||
DateTime asOf,
|
||||
int childIndex)
|
||||
int childIndex,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var fatherGiven = FatherGivenOf(family, members);
|
||||
if (fatherGiven is null)
|
||||
@@ -169,7 +174,7 @@ internal static class FamilyFactory
|
||||
var (surnameNom, surnameCases) = SurnameFor(family, names, members, female);
|
||||
var (numbers, choices) = PersonSampler.Body(catalog, rng, female, age);
|
||||
var traits = PersonSampler.Traits(catalog, rng, [PersonRoles.Student], age);
|
||||
var skills = PersonSampler.Skills(catalog, rng, age, choices, traits);
|
||||
var skills = PersonSampler.Skills(catalog, names, rng, age, choices, traits, seat.Year, adult: false, nativeLanguage);
|
||||
var needs = PersonSampler.Needs(catalog);
|
||||
var name = new PersonName(
|
||||
given.Form,
|
||||
@@ -209,7 +214,8 @@ internal static class FamilyFactory
|
||||
int schoolSeed,
|
||||
int familyIndex,
|
||||
DateTime asOf,
|
||||
string idPrefix = "a")
|
||||
string idPrefix = "a",
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var rng = new Random(Seed.Mix(schoolSeed, familyIndex, Seed.AppearanceSalt));
|
||||
var familyId = $"{idPrefix}{familyIndex}";
|
||||
@@ -232,7 +238,8 @@ internal static class FamilyFactory
|
||||
surname,
|
||||
given,
|
||||
NameGrammar.Patronymic(patronymicSource.Form, female, names.PatronymicRule),
|
||||
isParent: false);
|
||||
isParent: false,
|
||||
nativeLanguage);
|
||||
|
||||
return (new Family(familyId, [id], [], NextChild: 0, FatherGiven: string.Empty, Surname: surname.Male), person);
|
||||
}
|
||||
@@ -249,7 +256,8 @@ internal static class FamilyFactory
|
||||
SurnameEntry surname,
|
||||
GivenNameEntry given,
|
||||
string patronymic,
|
||||
bool isParent)
|
||||
bool isParent,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var age = SchoolYears.AgeYears(birth, asOf);
|
||||
var roles = isParent ? new[] { PersonRoles.Parent } : new[] { PersonRoles.Staff };
|
||||
@@ -269,7 +277,9 @@ internal static class FamilyFactory
|
||||
roles,
|
||||
isStudent: false,
|
||||
isParent,
|
||||
classId: null);
|
||||
classId: null,
|
||||
pupilYear: null,
|
||||
nativeLanguage);
|
||||
}
|
||||
|
||||
private static Person RollChild(
|
||||
@@ -281,7 +291,8 @@ internal static class FamilyFactory
|
||||
ChildDraft draft,
|
||||
DateTime asOf,
|
||||
SurnameEntry surname,
|
||||
string fatherGiven)
|
||||
string fatherGiven,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var age = SchoolYears.AgeYears(draft.Birth, asOf);
|
||||
var patronymic = NameGrammar.Patronymic(fatherGiven, draft.Female, names.PatronymicRule);
|
||||
@@ -301,7 +312,9 @@ internal static class FamilyFactory
|
||||
[PersonRoles.Student],
|
||||
isStudent: true,
|
||||
isParent: false,
|
||||
draft.Seat.ClassId);
|
||||
draft.Seat.ClassId,
|
||||
draft.Seat.Year,
|
||||
nativeLanguage);
|
||||
}
|
||||
|
||||
private static Person FinishPerson(
|
||||
@@ -320,11 +333,22 @@ internal static class FamilyFactory
|
||||
IReadOnlyList<string> roles,
|
||||
bool isStudent,
|
||||
bool isParent,
|
||||
string? classId)
|
||||
string? classId,
|
||||
int? pupilYear,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var (numbers, choices) = PersonSampler.Body(catalog, rng, female, age);
|
||||
var traits = PersonSampler.Traits(catalog, rng, roles, age);
|
||||
var skills = PersonSampler.Skills(catalog, rng, age, choices, traits);
|
||||
var skills = PersonSampler.Skills(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
age,
|
||||
choices,
|
||||
traits,
|
||||
isStudent ? pupilYear : null,
|
||||
adult: !isStudent,
|
||||
nativeLanguage);
|
||||
var needs = PersonSampler.Needs(catalog);
|
||||
var name = new PersonName(
|
||||
given.Form,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>
|
||||
/// A name set can speak several native languages; the school picks one. Empty request rolls from
|
||||
/// the school seed so the same create always lands on the same tongue.
|
||||
/// </summary>
|
||||
public static class NativeLanguages
|
||||
{
|
||||
public static bool Allows(NameSetDef names, string skill) =>
|
||||
names.Spoken.Contains(skill, StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="requested"/> must already be in the set, or empty. Invalid ids return
|
||||
/// null so the create path can 400. When omitted, a new school rolls; a reload without a
|
||||
/// saved pick takes the first listed language so old saves do not reshuffle intake.
|
||||
/// </summary>
|
||||
public static string? Pick(NameSetDef names, int schoolSeed, string? requested, bool rollIfOmitted)
|
||||
{
|
||||
var spoken = names.Spoken;
|
||||
if (spoken.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(requested))
|
||||
{
|
||||
return Allows(names, requested) ? requested : null;
|
||||
}
|
||||
|
||||
if (!rollIfOmitted || spoken.Count == 1)
|
||||
{
|
||||
return spoken[0];
|
||||
}
|
||||
|
||||
var rng = new Random(Seed.ForSchool(schoolSeed, Seed.NativeLanguageSalt));
|
||||
return spoken[rng.Next(spoken.Count)];
|
||||
}
|
||||
}
|
||||
@@ -35,28 +35,275 @@ internal static class PersonSampler
|
||||
|
||||
public static Dictionary<string, int> Skills(
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits)
|
||||
IReadOnlyList<string> traits,
|
||||
int? pupilYear,
|
||||
bool adult,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var values = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract)
|
||||
if (!skill.Abstract && skill.Always)
|
||||
{
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
|
||||
GrantNative(values, catalog, NativeOf(names, nativeLanguage), rng, age, choices);
|
||||
if (pupilYear is { } year)
|
||||
{
|
||||
GrantYearSubjects(values, catalog, rng, age, choices, year);
|
||||
}
|
||||
|
||||
if (adult)
|
||||
{
|
||||
GrantAdultExtras(values, catalog, rng, age, choices);
|
||||
}
|
||||
|
||||
ApplyTraitsAndBody(values, catalog, choices, traits, values.Keys.ToArray());
|
||||
// After traits so Diligent cannot push a related tongue past the "knows a bit" cap.
|
||||
GrantRelated(values, catalog, names, NativeOf(names, nativeLanguage), rng, choices);
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds subject skills a pupil just became old enough for. Existing values stay put — yearly
|
||||
/// intake must not re-roll mathematics every September.
|
||||
/// </summary>
|
||||
public static Dictionary<string, int> EnsurePupilYear(
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits,
|
||||
IReadOnlyDictionary<string, int> existing,
|
||||
int pupilYear,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var values = new Dictionary<string, int>(existing, StringComparer.Ordinal);
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract || !skill.Always || values.ContainsKey(skill.DefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mean = MeanForAge(skill, age);
|
||||
var stdDev = skill.Distribution?.StdDev ?? 10f;
|
||||
var rolled = (int)Math.Round(mean + (stdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
values[skill.DefName] = rolled;
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
|
||||
GrantNative(values, catalog, NativeOf(names, nativeLanguage), rng, age, choices);
|
||||
GrantYearSubjects(values, catalog, rng, age, choices, pupilYear);
|
||||
|
||||
var added = values.Keys.Where(name => !existing.ContainsKey(name)).ToArray();
|
||||
ApplyTraitsAndBody(values, catalog, choices, traits, added);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static string? NativeOf(NameSetDef? names, string? nativeLanguage)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(nativeLanguage))
|
||||
{
|
||||
return nativeLanguage;
|
||||
}
|
||||
|
||||
var spoken = names?.Spoken;
|
||||
return spoken is { Count: > 0 } ? spoken[0] : null;
|
||||
}
|
||||
|
||||
private static void GrantNative(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
string? native,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(native)
|
||||
|| !catalog.Skills.TryGetValue(native, out var skill)
|
||||
|| skill.Abstract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Other tongues of the same name set, at a low level. Skips keys already granted as native
|
||||
/// or by a subject — a Belarusian pupil who studies Russian keeps the school value.
|
||||
/// </summary>
|
||||
private static void GrantRelated(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
NameSetDef? names,
|
||||
string? native,
|
||||
Random rng,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
if (names is null || names.RelatedLanguageChance <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var name in names.Spoken.OrderBy(candidate => candidate, StringComparer.Ordinal))
|
||||
{
|
||||
if (name.Equals(native, StringComparison.Ordinal)
|
||||
|| values.ContainsKey(name)
|
||||
|| !catalog.Skills.TryGetValue(name, out var skill)
|
||||
|| skill.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rng.NextDouble() >= names.RelatedLanguageChance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rolled = (int)Math.Round(names.RelatedLanguageMean + (names.RelatedLanguageStdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = Math.Min(rolled, names.RelatedLanguageMax);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
values[skill.DefName] = Clamp(rolled, skill.Range);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantYearSubjects(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
int pupilYear)
|
||||
{
|
||||
foreach (var subject in catalog.Subjects.Values.OrderBy(def => def.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (subject.Abstract || pupilYear < subject.Grades.Min || pupilYear > subject.Grades.Max)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var share in subject.Skills.OrderBy(row => row.Skill, StringComparer.Ordinal))
|
||||
{
|
||||
if (!catalog.Skills.TryGetValue(share.Skill, out var skill) || skill.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantAdultExtras(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices)
|
||||
{
|
||||
var academic = catalog.Skills.Values
|
||||
.Where(skill =>
|
||||
!skill.Abstract
|
||||
&& !skill.Always
|
||||
&& !skill.Work
|
||||
&& skill.AdultChance <= 0
|
||||
&& SubjectUses(catalog, skill.DefName))
|
||||
.OrderBy(skill => skill.DefName, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
PickInto(values, rng, age, choices, academic, rng.Next(3));
|
||||
|
||||
var work = catalog.Skills.Values
|
||||
.Where(skill => !skill.Abstract && skill.Work)
|
||||
.OrderBy(skill => skill.DefName, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
PickInto(values, rng, age, choices, work, rng.Next(3));
|
||||
|
||||
foreach (var skill in catalog.Skills.Values.OrderBy(candidate => candidate.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (skill.Abstract || skill.AdultChance <= 0 || values.ContainsKey(skill.DefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rng.NextDouble() < skill.AdultChance)
|
||||
{
|
||||
RollInto(values, rng, age, choices, skill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SubjectUses(DefCatalog catalog, string skillName)
|
||||
{
|
||||
foreach (var subject in catalog.Subjects.Values)
|
||||
{
|
||||
if (subject.Abstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var share in subject.Skills)
|
||||
{
|
||||
if (share.Skill.Equals(skillName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void PickInto(
|
||||
Dictionary<string, int> values,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
List<SkillDef> pool,
|
||||
int count)
|
||||
{
|
||||
var remaining = pool.Where(skill => !values.ContainsKey(skill.DefName)).ToList();
|
||||
for (var n = 0; n < count && remaining.Count > 0; n++)
|
||||
{
|
||||
var index = rng.Next(remaining.Count);
|
||||
RollInto(values, rng, age, choices, remaining[index]);
|
||||
remaining.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RollInto(
|
||||
Dictionary<string, int> values,
|
||||
Random rng,
|
||||
int age,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
SkillDef skill)
|
||||
{
|
||||
if (values.ContainsKey(skill.DefName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mean = MeanForAge(skill, age);
|
||||
var stdDev = skill.Distribution?.StdDev ?? 10f;
|
||||
var rolled = (int)Math.Round(mean + (stdDev * NextGaussian(rng)));
|
||||
rolled = Clamp(rolled, skill.Range);
|
||||
rolled = ApplyBodyLimits(rolled, skill, choices);
|
||||
values[skill.DefName] = Clamp(rolled, skill.Range);
|
||||
}
|
||||
|
||||
private static void ApplyTraitsAndBody(
|
||||
Dictionary<string, int> values,
|
||||
DefCatalog catalog,
|
||||
IReadOnlyDictionary<string, string> choices,
|
||||
IReadOnlyList<string> traits,
|
||||
IEnumerable<string> names)
|
||||
{
|
||||
var allowed = names as ISet<string> ?? new HashSet<string>(names, StringComparer.Ordinal);
|
||||
var touched = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var traitName in traits)
|
||||
{
|
||||
@@ -67,7 +314,8 @@ internal static class PersonSampler
|
||||
|
||||
foreach (var modifier in trait.SkillModifiers)
|
||||
{
|
||||
if (!values.TryGetValue(modifier.Skill, out var current)
|
||||
if (!allowed.Contains(modifier.Skill)
|
||||
|| !values.TryGetValue(modifier.Skill, out var current)
|
||||
|| !catalog.Skills.TryGetValue(modifier.Skill, out var skill))
|
||||
{
|
||||
continue;
|
||||
@@ -86,8 +334,6 @@ internal static class PersonSampler
|
||||
var skill = catalog.Skills[skillName];
|
||||
values[skillName] = Clamp(ApplyBodyLimits(values[skillName], skill, choices), skill.Range);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static List<string> Traits(DefCatalog catalog, Random rng, IReadOnlyList<string> roles, int age)
|
||||
|
||||
@@ -16,7 +16,8 @@ public static class RosterGenerator
|
||||
MapLayout map,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime? asOf = null)
|
||||
DateTime? asOf = null,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(map);
|
||||
@@ -27,6 +28,7 @@ public static class RosterGenerator
|
||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
||||
}
|
||||
|
||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: true);
|
||||
var when = DateTime.SpecifyKind(asOf ?? DefaultAsOf, DateTimeKind.Utc);
|
||||
var yearStart = SchoolYears.StartOn(when);
|
||||
var demand = SchoolDemand.From(catalog, map);
|
||||
@@ -36,7 +38,7 @@ public static class RosterGenerator
|
||||
var families = new List<Family>(plans.Count);
|
||||
foreach (var plan in plans)
|
||||
{
|
||||
var (family, members) = FamilyFactory.Create(catalog, names, schoolSeed, plan, yearStart, when);
|
||||
var (family, members) = FamilyFactory.Create(catalog, names, schoolSeed, plan, yearStart, when, native);
|
||||
families.Add(family);
|
||||
people.AddRange(members);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ public static class Seed
|
||||
public const int HouseholdSalt = 5;
|
||||
public const int ApplicantSalt = 6;
|
||||
public const int CommuteSalt = 7;
|
||||
public const int SkillGrantSalt = 8;
|
||||
public const int NativeLanguageSalt = 9;
|
||||
|
||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
||||
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||
|
||||
@@ -39,7 +39,8 @@ public static class YearlyIntake
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
string nameSetId,
|
||||
DateTime asOf)
|
||||
DateTime asOf,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
@@ -50,6 +51,8 @@ public static class YearlyIntake
|
||||
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
||||
}
|
||||
|
||||
var native = NativeLanguages.Pick(names, schoolSeed, nativeLanguage, rollIfOmitted: false);
|
||||
|
||||
if (roster.Classes.Count == 0)
|
||||
{
|
||||
return roster;
|
||||
@@ -107,6 +110,10 @@ public static class YearlyIntake
|
||||
|
||||
var classes = new SchoolClass[roster.Classes.Count];
|
||||
var newSeats = new List<PupilSeat>();
|
||||
var incumbents = remainingPeople
|
||||
.Where(pair => pair.Value.IsStudent)
|
||||
.Select(pair => pair.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
for (var i = 0; i < roster.Classes.Count; i++)
|
||||
{
|
||||
var schoolClass = roster.Classes[i];
|
||||
@@ -134,13 +141,56 @@ public static class YearlyIntake
|
||||
families,
|
||||
remainingPeople,
|
||||
newSeats,
|
||||
nextFamilyIndex: roster.Families.Select(family => IndexOf(family.Id)).DefaultIfEmpty(-1).Max() + 1);
|
||||
nextFamilyIndex: roster.Families.Select(family => IndexOf(family.Id)).DefaultIfEmpty(-1).Max() + 1,
|
||||
native);
|
||||
|
||||
GrantPromotedSkills(catalog, names, schoolSeed, when, remainingPeople, classes, incumbents, native);
|
||||
|
||||
var people = remainingPeople.Values.ToList();
|
||||
var filled = AttachPupils(classes, people);
|
||||
return new Roster(people, families, filled);
|
||||
}
|
||||
|
||||
private static void GrantPromotedSkills(
|
||||
DefCatalog catalog,
|
||||
NameSetDef names,
|
||||
int schoolSeed,
|
||||
DateTime when,
|
||||
Dictionary<string, Person> people,
|
||||
SchoolClass[] classes,
|
||||
HashSet<string> incumbents,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var yearByClass = classes.ToDictionary(
|
||||
schoolClass => schoolClass.Id,
|
||||
schoolClass => schoolClass.Year,
|
||||
StringComparer.Ordinal);
|
||||
foreach (var id in incumbents)
|
||||
{
|
||||
if (!people.TryGetValue(id, out var person)
|
||||
|| person.ClassId is not { } classId
|
||||
|| !yearByClass.TryGetValue(classId, out var year))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rng = new Random(Seed.Mix(schoolSeed, person.Id, when.Year, Seed.SkillGrantSalt));
|
||||
people[id] = person with
|
||||
{
|
||||
Skills = PersonSampler.EnsurePupilYear(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
person.AgeOn(when),
|
||||
person.Choices,
|
||||
person.Traits,
|
||||
person.Skills,
|
||||
year,
|
||||
nativeLanguage),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillFirstYear(
|
||||
DefCatalog catalog,
|
||||
NameSetDef names,
|
||||
@@ -150,7 +200,8 @@ public static class YearlyIntake
|
||||
List<Family> families,
|
||||
Dictionary<string, Person> people,
|
||||
List<PupilSeat> seats,
|
||||
int nextFamilyIndex)
|
||||
int nextFamilyIndex,
|
||||
string? nativeLanguage)
|
||||
{
|
||||
var cursor = 0;
|
||||
foreach (var family in families.ToArray())
|
||||
@@ -185,7 +236,7 @@ public static class YearlyIntake
|
||||
|
||||
var childIndex = family.NextChildIndex;
|
||||
var rng = new Random(Seed.Mix(schoolSeed, index, Seed.IntakeSalt + yearStart.Year * 10 + childIndex));
|
||||
var child = FamilyFactory.AddChild(catalog, names, rng, family, members, seats[cursor], yearStart, asOf, childIndex);
|
||||
var child = FamilyFactory.AddChild(catalog, names, rng, family, members, seats[cursor], yearStart, asOf, childIndex, nativeLanguage);
|
||||
people[child.Id] = child;
|
||||
var familyAt = families.FindIndex(candidate => candidate.Id.Equals(family.Id, StringComparison.Ordinal));
|
||||
families[familyAt] = family with
|
||||
@@ -209,7 +260,7 @@ public static class YearlyIntake
|
||||
var leftover = seats.Skip(cursor).ToArray();
|
||||
foreach (var plan in FamilyPlanner.Singletons(leftover, Math.Max(nextFamilyIndex, 0)))
|
||||
{
|
||||
var (created, members) = FamilyFactory.Create(catalog, names, schoolSeed, plan, yearStart, asOf);
|
||||
var (created, members) = FamilyFactory.Create(catalog, names, schoolSeed, plan, yearStart, asOf, nativeLanguage);
|
||||
families.Add(created);
|
||||
foreach (var member in members)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user