Files
h-school/tests/HSchool.Server.Tests/PortraitSceneCollectorTests.cs
T

311 lines
13 KiB
C#

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("<embed:", StringComparison.Ordinal));
}
[Fact]
public void CollectThenBuild_PutsBlackboardAfterClothing_ChairRoomDoesNot()
{
var catalog = LoadCatalog();
var map = SampleMap();
var profile = SwarmUiPresetDefinition.CreateDefault().ToProfile(PortraitKind.Avatar);
var card = SampleWornCard();
var withBoard = PortraitSceneCollector.Collect(
catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget);
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: withBoard);
var clothesAt = positive.IndexOf("wearing a white shirt", StringComparison.OrdinalIgnoreCase);
var sceneAt = positive.IndexOf("blackboard", StringComparison.OrdinalIgnoreCase);
Assert.True(clothesAt >= 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" }],
},
],
};
}