Merge branch 'phase/35-appropriateness'
# Conflicts: # docs/phases/README.md
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class DressRulesApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task NewSchool_DefaultRules_AreRegularNoBright()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Правила дефолт", Start, seed: 35);
|
||||
|
||||
var response = await client.GetAsync($"/api/schools/{school.Id}/dress-rules", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rules = await response.Content.ReadFromJsonAsync<DressRulesDto>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(rules);
|
||||
Assert.Equal("regular", rules.Students.Form);
|
||||
Assert.Equal("noBright", rules.Students.Color);
|
||||
Assert.Equal("regular", rules.Staff.Form);
|
||||
Assert.Equal("noBright", rules.Staff.Color);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostStrictToday_DoesNotChangeWornUntilNextMorning()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Правила завтра", Start, seed: 35);
|
||||
var people = await client.GetFromJsonAsync<PeopleListResponse>(
|
||||
$"/api/schools/{school.Id}/people?role=student&pageSize=100",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(people);
|
||||
var pupil = people.People.First(row => row.Roles.Contains("student"));
|
||||
var before = await client.GetFromJsonAsync<PersonCardResponse>(
|
||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(pupil.Id)}?lang=ru",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(before);
|
||||
|
||||
var post = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/dress-rules",
|
||||
new { students = new { form = "strict", color = "noBright" } },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, post.StatusCode);
|
||||
|
||||
var after = await client.GetFromJsonAsync<PersonCardResponse>(
|
||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(pupil.Id)}?lang=ru",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(after);
|
||||
Assert.Equal(before.Worn.Select(row => row.DefName), after.Worn.Select(row => row.DefName));
|
||||
}
|
||||
|
||||
private sealed record DressRulesDto(DressRulePairDto Students, DressRulePairDto Staff);
|
||||
|
||||
private sealed record DressRulePairDto(string Form, string Color);
|
||||
|
||||
private sealed record PeopleListResponse(IReadOnlyList<PersonListItemResponse> People);
|
||||
|
||||
private sealed record PersonListItemResponse(string Id, IReadOnlyList<string> Roles);
|
||||
|
||||
private sealed record PersonCardResponse(IReadOnlyList<WornItemResponse> Worn);
|
||||
|
||||
private sealed record WornItemResponse(string DefName);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.Content.Tests;
|
||||
|
||||
public class AppropriatenessTests
|
||||
{
|
||||
[Fact]
|
||||
public void NoBright_RejectsBrightTag()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var rules = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright);
|
||||
Assert.False(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["TShirt"], "Red"));
|
||||
Assert.True(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["TShirt"], "Blue"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortForm_YoungPupil_UsesRegularEffectiveForm()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var rules = new DressRulePair(FormPolicies.Short, ColorPolicies.Free);
|
||||
var context = new ApparelContext(
|
||||
Female: true,
|
||||
Age: 7,
|
||||
IsStudent: true,
|
||||
rules,
|
||||
ApparelMode.Everyday,
|
||||
OutdoorTemperatureC: 15f,
|
||||
catalog.BehaviorRules);
|
||||
Assert.Equal(FormPolicies.Regular, Appropriateness.EffectiveForm(context));
|
||||
Assert.False(Appropriateness.SkirtLengthFits(context, catalog.Things["ShortSkirt"]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortForm_OlderPupil_AllowsShortSkirt()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var rules = new DressRulePair(FormPolicies.Short, ColorPolicies.Free);
|
||||
var context = new ApparelContext(
|
||||
Female: true,
|
||||
Age: 15,
|
||||
IsStudent: true,
|
||||
rules,
|
||||
ApparelMode.Everyday,
|
||||
OutdoorTemperatureC: 15f,
|
||||
catalog.BehaviorRules);
|
||||
Assert.True(Appropriateness.SkirtLengthFits(context, catalog.Things["ShortSkirt"]));
|
||||
}
|
||||
|
||||
private static DefCatalog LoadCatalog()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
var documents = PackDocuments.FromDirectory(CatalogLoader.CorePackId, root);
|
||||
return new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
using HSchool.Schedule;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Simulation.Tests;
|
||||
|
||||
public class AppropriatenessSimulationTests
|
||||
{
|
||||
private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime JanuaryMorning = new(2012, 1, 10, 6, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime JulyMorning = new(2012, 7, 10, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void Morning_NoBrightPolicy_WornColorsAreNotBright()
|
||||
{
|
||||
using var school = Open(TuesdayMorning, seed: 31);
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
Students = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright),
|
||||
};
|
||||
MorningDress.Apply(school);
|
||||
|
||||
foreach (var pupil in school.Roster!.People.Where(person => person.IsStudent))
|
||||
{
|
||||
foreach (var item in pupil.Items.Where(row => row.Location == ItemLocations.Worn))
|
||||
{
|
||||
if (item.Color is { } color
|
||||
&& school.Catalog!.Colors.TryGetValue(color, out var colorDef))
|
||||
{
|
||||
Assert.DoesNotContain(ColorTags.Bright, colorDef.Tags, StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrostMorning_WornOuterExists_JulyHasNoFurCoat()
|
||||
{
|
||||
using var january = Open(JanuaryMorning, seed: 31);
|
||||
MorningDress.Apply(january);
|
||||
Assert.Contains(
|
||||
january.Roster!.People.Where(person => person.IsStudent),
|
||||
person => person.Items.Any(item =>
|
||||
item.Location == ItemLocations.Worn
|
||||
&& january.Catalog!.Things[item.Def].Layers.Any(layer =>
|
||||
layer.Equals(ApparelLayers.Outer, StringComparison.Ordinal))));
|
||||
|
||||
using var july = Open(JulyMorning, seed: 31);
|
||||
july.SyncWeather(force: true);
|
||||
MorningDress.Apply(july);
|
||||
Assert.DoesNotContain(
|
||||
july.Roster!.People.SelectMany(person => person.Items),
|
||||
item => item.Location == ItemLocations.Worn && item.Def == "FurCoat");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeBreak_PupilReachesOwnChangingRoom()
|
||||
{
|
||||
var (school, pupilId) = StaffedPeClass();
|
||||
using (school)
|
||||
{
|
||||
MorningDress.Apply(school);
|
||||
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 24, 0, DateTimeKind.Utc));
|
||||
var row = school.CapturePresence().Single(item => item.PersonId == pupilId);
|
||||
var person = school.Roster!.People.Single(item => item.Id == pupilId);
|
||||
var room = person.Female ? "girls-changing-room" : "boys-changing-room";
|
||||
Assert.Equal(room, row.NodeId);
|
||||
Assert.Empty(row.Path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Boy_DoesNotEnterGirlsChangingRoom()
|
||||
{
|
||||
var (school, pupilId) = StaffedPeClass(forceBoy: true);
|
||||
using (school)
|
||||
{
|
||||
MorningDress.Apply(school);
|
||||
AdvanceTo(school, new DateTime(2012, 4, 3, 9, 18, 0, DateTimeKind.Utc));
|
||||
var row = school.CapturePresence().Single(item => item.PersonId == pupilId);
|
||||
Assert.NotEqual("girls-changing-room", row.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeWithoutLocker_StashedItemGoesToBag()
|
||||
{
|
||||
using var school = Open(TuesdayMorning, seed: 31);
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
Students = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright),
|
||||
};
|
||||
var pupil = school.Roster!.People.First(person => person.IsStudent && person.LockerRoomId is null);
|
||||
var items = pupil.Items.ToList();
|
||||
items.Add(new InventoryItem("TShirt", "Red", 1f, ItemLocations.Worn));
|
||||
ReplaceItems(pupil, items);
|
||||
ApparelDresser.RedressPerson(school, pupil, ApparelMode.Everyday, includeHome: false);
|
||||
pupil = school.Roster.People.Single(person => person.Id == pupil.Id);
|
||||
Assert.Contains(pupil.Items, item => item.Def == "TShirt" && item.Color == "Red" && item.Location == ItemLocations.Bag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingStrictRule_DoesNotApplyUntilNextMorning()
|
||||
{
|
||||
using var school = Open(TuesdayMorning, seed: 31);
|
||||
MorningDress.Apply(school);
|
||||
var before = BrightWornCount(school);
|
||||
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
PendingStudents = new DressRulePair(FormPolicies.Strict, ColorPolicies.NoBright),
|
||||
};
|
||||
MorningDress.Apply(school);
|
||||
Assert.Equal(before, BrightWornCount(school));
|
||||
|
||||
school.Clock.JumpTo(new DateTime(2012, 4, 4, 6, 0, 0, DateTimeKind.Utc));
|
||||
ApparelWear.Apply(school, gameMinutes: 0, before: new DateTime(2012, 4, 3, 22, 0, 0, DateTimeKind.Utc));
|
||||
Assert.Equal(0, BrightWornCount(school));
|
||||
}
|
||||
|
||||
private static int BrightWornCount(School school)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var pupil in school.Roster!.People.Where(person => person.IsStudent))
|
||||
{
|
||||
foreach (var item in pupil.Items.Where(row => row.Location == ItemLocations.Worn))
|
||||
{
|
||||
if (item.Color is { } color
|
||||
&& school.Catalog!.Colors.TryGetValue(color, out var colorDef)
|
||||
&& colorDef.Tags.Contains(ColorTags.Bright, StringComparer.Ordinal))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void ReplaceItems(Person person, List<InventoryItem> items)
|
||||
{
|
||||
if (person.Items is IList<InventoryItem> list && !list.IsReadOnly)
|
||||
{
|
||||
list.Clear();
|
||||
foreach (var item in items)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (School School, string PupilId) StaffedPeClass(bool forceBoy = false)
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||
roster = LockerAssigner.Apply(catalog, map, roster);
|
||||
roster = DressGenerator.EnsureRoster(catalog, roster, schoolSeed: 1, TuesdayMorning);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning);
|
||||
var schoolClass = roster.Classes.First(row =>
|
||||
row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104");
|
||||
var pupil = roster.People
|
||||
.Where(person => schoolClass.PupilIds.Contains(person.Id))
|
||||
.First(person => forceBoy ? !person.Female : true);
|
||||
var school = School.Create(1, "PE", TuesdayMorning, catalog, map);
|
||||
school.InstallPeople(roster, seed: 1, "Russia", pool, climatePresetId: "TemperateContinental");
|
||||
school.SetTimetable(new Timetable(
|
||||
[
|
||||
new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1),
|
||||
new LessonPlacement(schoolClass.Id, "PhysicalEducation", "t2", "gym-hall", Day: 1, Period: 2),
|
||||
],
|
||||
[]));
|
||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||
return (school, pupil.Id);
|
||||
}
|
||||
|
||||
private static School Open(DateTime start, int seed)
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", start);
|
||||
roster = LockerAssigner.Apply(catalog, map, roster);
|
||||
roster = DressGenerator.EnsureRoster(catalog, roster, seed, start);
|
||||
var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", start);
|
||||
var school = School.Create(1, "Одежда", start, catalog, map);
|
||||
school.InstallPeople(roster, seed, "Russia", pool, climatePresetId: "TemperateContinental");
|
||||
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000);
|
||||
return school;
|
||||
}
|
||||
|
||||
private static void AdvanceTo(School school, DateTime until)
|
||||
{
|
||||
while (school.Clock.Time < until)
|
||||
{
|
||||
school.Tick(0.2d, 5d);
|
||||
}
|
||||
}
|
||||
|
||||
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
var documents = new List<ContentDocument>();
|
||||
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
|
||||
}
|
||||
|
||||
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
||||
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
|
||||
Assert.NotNull(map);
|
||||
return (catalog, map);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user