using HSchool.Content; using HSchool.Server.Api; using HSchool.Server.Game; using HSchool.Simulation; namespace HSchool.Server.Tests; public class PortraitSceneCollectorTests { private static readonly SimulationOptions Budget = new(); private readonly CatalogLoader _loader = new(); [Fact] public void Classroom_IncludesBlackboardPhrase_OfficeWithoutBoardDoesNot() { var catalog = LoadCatalog(); var map = SampleMap(); var withBoard = PortraitSceneCollector.Collect( catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget); var withoutBoard = PortraitSceneCollector.Collect( catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget); Assert.Contains(withBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase)); } [Fact] public void FillBlackboard_IsIncluded_ChairFillIsSilent() { var catalog = LoadCatalog(); var map = SampleMap(); var office = PortraitSceneCollector.Collect( catalog, map, "office", Precipitation.None, "TestClimate", [], [], Budget); Assert.Contains(office.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(office.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase)); } [Fact] public void Snow_OnlyOutdoors() { var catalog = LoadCatalog(); var map = SampleMap(); var yard = PortraitSceneCollector.Collect( catalog, map, "yard", Precipitation.Snow, "TestClimate", [], [], Budget); var indoor = PortraitSceneCollector.Collect( catalog, map, "class-1", Precipitation.Snow, "TestClimate", [], [], Budget); Assert.Contains(yard.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase)); Assert.DoesNotContain(indoor.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase)); } [Fact] public void MoreThanSixCandidates_KeepsTopWeightsThenDefName() { var catalog = LoadCatalog(); var map = SampleMap(); var options = new SimulationOptions { PortraitSceneFragmentLimit = 6 }; var scene = PortraitSceneCollector.Collect( catalog, map, "ranked", Precipitation.None, "TestClimate", [], [], options); Assert.Equal(6, scene.PositiveFragments.Count); Assert.Contains("frag-w7", scene.PositiveFragments); Assert.Contains("frag-w2", scene.PositiveFragments); Assert.DoesNotContain("frag-w1", scene.PositiveFragments); Assert.Equal( ["frag-w7", "frag-w6", "frag-w5", "frag-w4", "frag-w3", "frag-w2"], scene.PositiveFragments); } [Fact] public void EqualWeight_BreaksTiesByDefName() { var catalog = LoadCatalog(); var map = SampleMap(); var options = new SimulationOptions { PortraitSceneFragmentLimit = 1 }; var scene = PortraitSceneCollector.Collect( catalog, map, "tie", Precipitation.None, "TestClimate", [], [], options); Assert.Equal(["alpha-tie"], scene.PositiveFragments); Assert.DoesNotContain("zeta-tie", scene.PositiveFragments); } [Fact] public void SceneLora_GoesToLoras_EmbeddingGoesToTextTags() { var catalog = LoadCatalog(); var map = SampleMap(); var scene = PortraitSceneCollector.Collect( catalog, map, "props", Precipitation.None, "TestClimate", [], [], Budget); Assert.Equal(["snow.safetensors"], scene.PositiveLoras.Select(entry => entry.Name)); Assert.Equal(["winter-embed"], scene.PositiveEmbeddings.Select(entry => entry.Name)); Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("snow.safetensors", StringComparison.Ordinal)); Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("= 0 && sceneAt > clothesAt); var chairs = PortraitSceneCollector.Collect( catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget); var (without, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: chairs); Assert.DoesNotContain("blackboard", without, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("chair", without, StringComparison.OrdinalIgnoreCase); } private static PersonCardResponse SampleWornCard() => new( "f0.c0", "Maria Ivanova", "Ivanova", "Maria", "", true, 12, new DateTime(2000, 3, 14, 0, 0, 0, DateTimeKind.Utc), ["student"], 5, "A", "class-1", null, null, [new LabeledStatResponse("HairColor", "Hair colour", "Black")], [], [], [], null, null, new PersonFamilyResponse([], [], [], []), [new WornItemResponse("Shirt", "Shirt", "White", "White", [new DefLabelResponse(ApparelLayers.Top, "Top")], 1f, "new", [ApparelLayers.Underwear])], [], 0f, 0f); private DefCatalog LoadCatalog() { return _loader.Load( [CatalogLoader.CorePackId], [ new ContentDocument( CatalogLoader.CorePackId, "defs/things/scene.jsonc", """ [ { "defName": "Blackboard", "prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 } }, { "defName": "Chair" }, { "defName": "StudentDesk", "pupilSlots": 1 }, { "defName": "W7", "prompt": { "positive": "frag-w7", "weight": 7 } }, { "defName": "W6", "prompt": { "positive": "frag-w6", "weight": 6 } }, { "defName": "W5", "prompt": { "positive": "frag-w5", "weight": 5 } }, { "defName": "W4", "prompt": { "positive": "frag-w4", "weight": 4 } }, { "defName": "W3", "prompt": { "positive": "frag-w3", "weight": 3 } }, { "defName": "W2", "prompt": { "positive": "frag-w2", "weight": 2 } }, { "defName": "W1", "prompt": { "positive": "frag-w1", "weight": 1 } }, { "defName": "AlphaTie", "prompt": { "positive": "alpha-tie", "weight": 1 } }, { "defName": "ZetaTie", "prompt": { "positive": "zeta-tie", "weight": 1 } }, { "defName": "Prop", "prompt": { "positive": "a winter prop", "weight": 1, "positiveLoras": [{ "name": "snow.safetensors", "weight": 0.7 }], "positiveEmbeddings": [{ "name": "winter-embed", "weight": 1 }] } } ] """), new ContentDocument( CatalogLoader.CorePackId, "defs/rooms/scene.jsonc", """ [ { "defName": "Classroom", "homeroom": true, "seatThing": "StudentDesk", "defaultSeats": 16, "travelMinutes": 0.5, "prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 } }, { "defName": "Office", "travelMinutes": 0.5, "slots": [ { "key": "board", "thing": "Blackboard" }, { "key": "seat", "thing": "Chair" }, { "key": "a", "thing": "W7" }, { "key": "b", "thing": "W6" }, { "key": "c", "thing": "W5" }, { "key": "d", "thing": "W4" }, { "key": "e", "thing": "W3" }, { "key": "f", "thing": "W2" }, { "key": "g", "thing": "W1" }, { "key": "alpha", "thing": "AlphaTie" }, { "key": "zeta", "thing": "ZetaTie" }, { "key": "prop", "thing": "Prop" } ] } ] """), new ContentDocument( CatalogLoader.CorePackId, "defs/territories/yard.jsonc", """{ "defName": "SchoolYard", "travelMinutes": 3 }"""), new ContentDocument( CatalogLoader.CorePackId, "defs/climates/test.jsonc", """ { "defName": "TestClimate", "monthlyNorms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "daySpread": 0, "hourSpread": 0, "precipitationChance": 0, "prompt": { "snow": { "positive": "falling snow, winter overcast schoolyard", "weight": 1 } } } """), ]); } private static MapLayout SampleMap() => new() { Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" }, Rooms = [ new RoomNode { Id = "class-1", Def = "Classroom", Building = "main", Floor = "f1", Seats = 16, }, new RoomNode { Id = "office", Def = "Office", Building = "main", Floor = "f1", Slots = [new SlotFill { Key = "board", Thing = "Blackboard" }], }, new RoomNode { Id = "chairs", Def = "Office", Building = "main", Floor = "f1", Slots = [new SlotFill { Key = "seat", Thing = "Chair" }], }, new RoomNode { Id = "ranked", Def = "Office", Building = "main", Floor = "f1", Slots = [ new SlotFill { Key = "a", Thing = "W7" }, new SlotFill { Key = "b", Thing = "W6" }, new SlotFill { Key = "c", Thing = "W5" }, new SlotFill { Key = "d", Thing = "W4" }, new SlotFill { Key = "e", Thing = "W3" }, new SlotFill { Key = "f", Thing = "W2" }, new SlotFill { Key = "g", Thing = "W1" }, ], }, new RoomNode { Id = "tie", Def = "Office", Building = "main", Floor = "f1", Slots = [ new SlotFill { Key = "zeta", Thing = "ZetaTie" }, new SlotFill { Key = "alpha", Thing = "AlphaTie" }, ], }, new RoomNode { Id = "props", Def = "Office", Building = "main", Floor = "f1", Slots = [new SlotFill { Key = "prop", Thing = "Prop" }], }, ], }; }